Spaces:
Running
Running
refactor: restructure demo to examples/robot-control-web
Browse files- .github/CONTRIBUTING.md +1 -1
- examples/{demo → robot-control-web}/App.tsx +0 -0
- examples/{demo → robot-control-web}/components/CalibrationModal.tsx +0 -0
- examples/{demo → robot-control-web}/components/CalibrationPanel.tsx +0 -0
- examples/{demo → robot-control-web}/components/ErrorBoundary.tsx +0 -0
- examples/{demo → robot-control-web}/components/PortManager.tsx +0 -0
- examples/{demo → robot-control-web}/components/TeleoperationPanel.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/alert.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/badge.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/button.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/card.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/dialog.tsx +0 -0
- examples/{demo → robot-control-web}/components/ui/progress.tsx +0 -0
- examples/{demo → robot-control-web}/index.css +0 -0
- examples/{demo → robot-control-web}/lib/unified-storage.ts +0 -0
- examples/{demo → robot-control-web}/lib/utils.ts +0 -0
- examples/{demo → robot-control-web}/main.tsx +0 -0
- examples/{demo → robot-control-web}/pages/Home.tsx +0 -0
- index.html +1 -1
- packages/web/src/index.ts +2 -0
- pnpm-lock.yaml +12 -0
- src/demo/App.tsx +0 -19
- src/demo/components/CalibrationModal.tsx +0 -49
- src/demo/components/CalibrationPanel.tsx +0 -419
- src/demo/components/ErrorBoundary.tsx +0 -65
- src/demo/components/PortManager.tsx +0 -1251
- src/demo/components/TeleoperationPanel.tsx +0 -530
- src/demo/components/ui/alert.tsx +0 -58
- src/demo/components/ui/badge.tsx +0 -35
- src/demo/components/ui/button.tsx +0 -53
- src/demo/components/ui/card.tsx +0 -85
- src/demo/components/ui/dialog.tsx +0 -120
- src/demo/components/ui/progress.tsx +0 -26
- src/demo/index.css +0 -12
- src/demo/lib/unified-storage.ts +0 -325
- src/demo/lib/utils.ts +0 -6
- src/demo/main.tsx +0 -5
- src/demo/pages/Home.tsx +0 -99
- src/demo/types.ts +0 -2
- tailwind.config.js +4 -1
- tsconfig.json +1 -1
.github/CONTRIBUTING.md
CHANGED
|
@@ -20,7 +20,7 @@ pnpm --filter "@lerobot/web" run build
|
|
| 20 |
## 📦 Package Structure
|
| 21 |
|
| 22 |
- **`packages/web/`** - Browser package (`@lerobot/web` on npm)
|
| 23 |
-
- **`
|
| 24 |
- **`src/cli/`** - Node.js CLI tool
|
| 25 |
- **`src/lerobot/node/`** - Node.js library
|
| 26 |
|
|
|
|
| 20 |
## 📦 Package Structure
|
| 21 |
|
| 22 |
- **`packages/web/`** - Browser package (`@lerobot/web` on npm)
|
| 23 |
+
- **`examples/robot-control-web/`** - Demo application (deployed to HF Spaces)
|
| 24 |
- **`src/cli/`** - Node.js CLI tool
|
| 25 |
- **`src/lerobot/node/`** - Node.js library
|
| 26 |
|
examples/{demo → robot-control-web}/App.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/CalibrationModal.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/CalibrationPanel.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ErrorBoundary.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/PortManager.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/TeleoperationPanel.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ui/alert.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ui/badge.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ui/button.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ui/card.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ui/dialog.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/components/ui/progress.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/index.css
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/lib/unified-storage.ts
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/lib/utils.ts
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/main.tsx
RENAMED
|
File without changes
|
examples/{demo → robot-control-web}/pages/Home.tsx
RENAMED
|
File without changes
|
index.html
CHANGED
|
@@ -76,6 +76,6 @@
|
|
| 76 |
</head>
|
| 77 |
<body>
|
| 78 |
<div id="root"></div>
|
| 79 |
-
<script type="module" src="/
|
| 80 |
</body>
|
| 81 |
</html>
|
|
|
|
| 76 |
</head>
|
| 77 |
<body>
|
| 78 |
<div id="root"></div>
|
| 79 |
+
<script type="module" src="/examples/robot-control-web/main.tsx"></script>
|
| 80 |
</body>
|
| 81 |
</html>
|
packages/web/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { calibrate, isWebSerialSupported } from "./calibrate.js";
|
|
| 10 |
export { teleoperate } from "./teleoperate.js";
|
| 11 |
export { findPort } from "./find_port.js";
|
| 12 |
|
|
|
|
|
|
|
| 13 |
// Types
|
| 14 |
export type {
|
| 15 |
RobotConnection,
|
|
|
|
| 10 |
export { teleoperate } from "./teleoperate.js";
|
| 11 |
export { findPort } from "./find_port.js";
|
| 12 |
|
| 13 |
+
console.log("asdfasdfasdfasdf");
|
| 14 |
+
|
| 15 |
// Types
|
| 16 |
export type {
|
| 17 |
RobotConnection,
|
pnpm-lock.yaml
CHANGED
|
@@ -8,6 +8,9 @@ importers:
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
|
|
|
|
|
|
|
|
|
| 11 |
'@radix-ui/react-dialog':
|
| 12 |
specifier: ^1.1.14
|
| 13 |
version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
@@ -399,6 +402,11 @@ packages:
|
|
| 399 |
'@jridgewell/trace-mapping@0.3.25':
|
| 400 |
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
'@manypkg/find-root@1.1.0':
|
| 403 |
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
|
| 404 |
|
|
@@ -2077,6 +2085,10 @@ snapshots:
|
|
| 2077 |
'@jridgewell/resolve-uri': 3.1.2
|
| 2078 |
'@jridgewell/sourcemap-codec': 1.5.0
|
| 2079 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2080 |
'@manypkg/find-root@1.1.0':
|
| 2081 |
dependencies:
|
| 2082 |
'@babel/runtime': 7.27.6
|
|
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
| 11 |
+
'@lerobot/web':
|
| 12 |
+
specifier: ^0.1.1
|
| 13 |
+
version: 0.1.1(typescript@5.8.3)
|
| 14 |
'@radix-ui/react-dialog':
|
| 15 |
specifier: ^1.1.14
|
| 16 |
version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
|
|
| 402 |
'@jridgewell/trace-mapping@0.3.25':
|
| 403 |
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
| 404 |
|
| 405 |
+
'@lerobot/web@0.1.1':
|
| 406 |
+
resolution: {integrity: sha512-8xLGBTIQQetJzqauM9OtiSUIaSLphCH2qAqiGVzszmJk7pAcucqouezcIGRHiDOVRFDfpAYerRyfoeFLdoKqDQ==}
|
| 407 |
+
peerDependencies:
|
| 408 |
+
typescript: '>=4.5.0'
|
| 409 |
+
|
| 410 |
'@manypkg/find-root@1.1.0':
|
| 411 |
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
|
| 412 |
|
|
|
|
| 2085 |
'@jridgewell/resolve-uri': 3.1.2
|
| 2086 |
'@jridgewell/sourcemap-codec': 1.5.0
|
| 2087 |
|
| 2088 |
+
'@lerobot/web@0.1.1(typescript@5.8.3)':
|
| 2089 |
+
dependencies:
|
| 2090 |
+
typescript: 5.8.3
|
| 2091 |
+
|
| 2092 |
'@manypkg/find-root@1.1.0':
|
| 2093 |
dependencies:
|
| 2094 |
'@babel/runtime': 7.27.6
|
src/demo/App.tsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
import { Home } from "./pages/Home";
|
| 3 |
-
import { ErrorBoundary } from "./components/ErrorBoundary";
|
| 4 |
-
import type { RobotConnection } from "@lerobot/web";
|
| 5 |
-
|
| 6 |
-
export function App() {
|
| 7 |
-
const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
|
| 8 |
-
|
| 9 |
-
return (
|
| 10 |
-
<ErrorBoundary>
|
| 11 |
-
<div className="min-h-screen bg-background">
|
| 12 |
-
<Home
|
| 13 |
-
connectedRobots={connectedRobots}
|
| 14 |
-
onConnectedRobotsChange={setConnectedRobots}
|
| 15 |
-
/>
|
| 16 |
-
</div>
|
| 17 |
-
</ErrorBoundary>
|
| 18 |
-
);
|
| 19 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/CalibrationModal.tsx
DELETED
|
@@ -1,49 +0,0 @@
|
|
| 1 |
-
import {
|
| 2 |
-
Dialog,
|
| 3 |
-
DialogContent,
|
| 4 |
-
DialogDescription,
|
| 5 |
-
DialogFooter,
|
| 6 |
-
DialogHeader,
|
| 7 |
-
DialogTitle,
|
| 8 |
-
} from "./ui/dialog";
|
| 9 |
-
import { Button } from "./ui/button";
|
| 10 |
-
|
| 11 |
-
interface CalibrationModalProps {
|
| 12 |
-
open: boolean;
|
| 13 |
-
onOpenChange: (open: boolean) => void;
|
| 14 |
-
deviceType: string;
|
| 15 |
-
onContinue: () => void;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export function CalibrationModal({
|
| 19 |
-
open,
|
| 20 |
-
onOpenChange,
|
| 21 |
-
deviceType,
|
| 22 |
-
onContinue,
|
| 23 |
-
}: CalibrationModalProps) {
|
| 24 |
-
return (
|
| 25 |
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 26 |
-
<DialogContent className="sm:max-w-md">
|
| 27 |
-
<DialogHeader>
|
| 28 |
-
<DialogTitle>📍 Set Homing Position</DialogTitle>
|
| 29 |
-
<DialogDescription className="text-base py-4">
|
| 30 |
-
Move the SO-100 {deviceType} to the <strong>MIDDLE</strong> of its
|
| 31 |
-
range of motion and click OK when ready.
|
| 32 |
-
<br />
|
| 33 |
-
<br />
|
| 34 |
-
The calibration will then automatically:
|
| 35 |
-
<br />• Record homing offsets
|
| 36 |
-
<br />• Record joint ranges (manual - you control when to stop)
|
| 37 |
-
<br />• Save configuration file
|
| 38 |
-
</DialogDescription>
|
| 39 |
-
</DialogHeader>
|
| 40 |
-
|
| 41 |
-
<DialogFooter>
|
| 42 |
-
<Button onClick={onContinue} className="w-full">
|
| 43 |
-
OK - Start Calibration
|
| 44 |
-
</Button>
|
| 45 |
-
</DialogFooter>
|
| 46 |
-
</DialogContent>
|
| 47 |
-
</Dialog>
|
| 48 |
-
);
|
| 49 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/CalibrationPanel.tsx
DELETED
|
@@ -1,419 +0,0 @@
|
|
| 1 |
-
import { useState, useCallback, useMemo } from "react";
|
| 2 |
-
import { Button } from "./ui/button";
|
| 3 |
-
import {
|
| 4 |
-
Card,
|
| 5 |
-
CardContent,
|
| 6 |
-
CardDescription,
|
| 7 |
-
CardHeader,
|
| 8 |
-
CardTitle,
|
| 9 |
-
} from "./ui/card";
|
| 10 |
-
import { Badge } from "./ui/badge";
|
| 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";
|
| 21 |
-
import type { RobotConnection } from "@lerobot/web";
|
| 22 |
-
|
| 23 |
-
interface CalibrationPanelProps {
|
| 24 |
-
robot: RobotConnection;
|
| 25 |
-
onFinish: () => void;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
|
| 29 |
-
// Simple state management
|
| 30 |
-
const [isCalibrating, setIsCalibrating] = useState(false);
|
| 31 |
-
const [calibrationResult, setCalibrationResult] =
|
| 32 |
-
useState<WebCalibrationResults | null>(null);
|
| 33 |
-
const [status, setStatus] = useState<string>("Ready to calibrate");
|
| 34 |
-
const [modalOpen, setModalOpen] = useState(false);
|
| 35 |
-
const [calibrationProcess, setCalibrationProcess] =
|
| 36 |
-
useState<CalibrationProcess | null>(null);
|
| 37 |
-
const [motorData, setMotorData] = useState<LiveCalibrationData>({});
|
| 38 |
-
const [isPreparing, setIsPreparing] = useState(false);
|
| 39 |
-
|
| 40 |
-
// Motor names for display
|
| 41 |
-
const motorNames = useMemo(
|
| 42 |
-
() => [
|
| 43 |
-
"shoulder_pan",
|
| 44 |
-
"shoulder_lift",
|
| 45 |
-
"elbow_flex",
|
| 46 |
-
"wrist_flex",
|
| 47 |
-
"wrist_roll",
|
| 48 |
-
"gripper",
|
| 49 |
-
],
|
| 50 |
-
[]
|
| 51 |
-
);
|
| 52 |
-
|
| 53 |
-
// Initialize motor data
|
| 54 |
-
const initializeMotorData = useCallback(() => {
|
| 55 |
-
const initialData: LiveCalibrationData = {};
|
| 56 |
-
motorNames.forEach((name) => {
|
| 57 |
-
initialData[name] = {
|
| 58 |
-
current: 2047,
|
| 59 |
-
min: 2047,
|
| 60 |
-
max: 2047,
|
| 61 |
-
range: 0,
|
| 62 |
-
};
|
| 63 |
-
});
|
| 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");
|
| 86 |
-
} catch (error) {
|
| 87 |
-
console.warn("Failed to release motor torque:", error);
|
| 88 |
-
setStatus("⚠️ Could not release motor torque - try moving joints gently");
|
| 89 |
-
} finally {
|
| 90 |
-
setIsPreparing(false);
|
| 91 |
-
}
|
| 92 |
-
}, [robot]);
|
| 93 |
-
|
| 94 |
-
// Start calibration using new API
|
| 95 |
-
const handleContinueCalibration = useCallback(async () => {
|
| 96 |
-
setModalOpen(false);
|
| 97 |
-
|
| 98 |
-
if (!robot.port || !robot.robotType) {
|
| 99 |
-
return;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
try {
|
| 103 |
-
setStatus("🤖 Starting calibration process...");
|
| 104 |
-
setIsCalibrating(true);
|
| 105 |
-
initializeMotorData();
|
| 106 |
-
|
| 107 |
-
// Use the simple calibrate API - just pass the robot connection
|
| 108 |
-
const process = await calibrate(robot, {
|
| 109 |
-
onLiveUpdate: (data) => {
|
| 110 |
-
setMotorData(data);
|
| 111 |
-
setStatus(
|
| 112 |
-
"📏 Recording joint ranges - move all joints through their full range"
|
| 113 |
-
);
|
| 114 |
-
},
|
| 115 |
-
onProgress: (message) => {
|
| 116 |
-
setStatus(message);
|
| 117 |
-
},
|
| 118 |
-
});
|
| 119 |
-
|
| 120 |
-
setCalibrationProcess(process);
|
| 121 |
-
|
| 122 |
-
// Add Enter key listener for stopping (matching Node.js UX)
|
| 123 |
-
const handleKeyPress = (event: KeyboardEvent) => {
|
| 124 |
-
if (event.key === "Enter") {
|
| 125 |
-
process.stop();
|
| 126 |
-
}
|
| 127 |
-
};
|
| 128 |
-
document.addEventListener("keydown", handleKeyPress);
|
| 129 |
-
|
| 130 |
-
try {
|
| 131 |
-
// Wait for calibration to complete
|
| 132 |
-
const result = await process.result;
|
| 133 |
-
setCalibrationResult(result);
|
| 134 |
-
|
| 135 |
-
// App-level concern: Save results to storage
|
| 136 |
-
const serialNumber =
|
| 137 |
-
robot.serialNumber || robot.usbMetadata?.serialNumber || "unknown";
|
| 138 |
-
await saveCalibrationResults(
|
| 139 |
-
result,
|
| 140 |
-
robot.robotType,
|
| 141 |
-
robot.robotId || `${robot.robotType}_1`,
|
| 142 |
-
serialNumber
|
| 143 |
-
);
|
| 144 |
-
|
| 145 |
-
setStatus(
|
| 146 |
-
"✅ Calibration completed successfully! Configuration saved."
|
| 147 |
-
);
|
| 148 |
-
} finally {
|
| 149 |
-
document.removeEventListener("keydown", handleKeyPress);
|
| 150 |
-
setCalibrationProcess(null);
|
| 151 |
-
setIsCalibrating(false);
|
| 152 |
-
}
|
| 153 |
-
} catch (error) {
|
| 154 |
-
console.error("❌ Calibration failed:", error);
|
| 155 |
-
setStatus(
|
| 156 |
-
`❌ Calibration failed: ${
|
| 157 |
-
error instanceof Error ? error.message : error
|
| 158 |
-
}`
|
| 159 |
-
);
|
| 160 |
-
setIsCalibrating(false);
|
| 161 |
-
setCalibrationProcess(null);
|
| 162 |
-
}
|
| 163 |
-
}, [robot, initializeMotorData]);
|
| 164 |
-
|
| 165 |
-
// Stop calibration recording
|
| 166 |
-
const handleStopRecording = useCallback(() => {
|
| 167 |
-
if (calibrationProcess) {
|
| 168 |
-
calibrationProcess.stop();
|
| 169 |
-
}
|
| 170 |
-
}, [calibrationProcess]);
|
| 171 |
-
|
| 172 |
-
// App-level concern: Save calibration results
|
| 173 |
-
const saveCalibrationResults = async (
|
| 174 |
-
results: WebCalibrationResults,
|
| 175 |
-
robotType: string,
|
| 176 |
-
robotId: string,
|
| 177 |
-
serialNumber: string
|
| 178 |
-
) => {
|
| 179 |
-
try {
|
| 180 |
-
// Save to unified storage (app-level functionality)
|
| 181 |
-
const { saveCalibrationData } = await import("../lib/unified-storage.js");
|
| 182 |
-
|
| 183 |
-
const fullCalibrationData = {
|
| 184 |
-
...results,
|
| 185 |
-
device_type: robotType,
|
| 186 |
-
device_id: robotId,
|
| 187 |
-
calibrated_at: new Date().toISOString(),
|
| 188 |
-
platform: "web",
|
| 189 |
-
api: "Web Serial API",
|
| 190 |
-
};
|
| 191 |
-
|
| 192 |
-
const metadata = {
|
| 193 |
-
timestamp: new Date().toISOString(),
|
| 194 |
-
readCount: Object.keys(motorData).length > 0 ? 100 : 0, // Estimate
|
| 195 |
-
};
|
| 196 |
-
|
| 197 |
-
saveCalibrationData(serialNumber, fullCalibrationData, metadata);
|
| 198 |
-
} catch (error) {
|
| 199 |
-
console.warn("Failed to save calibration results:", error);
|
| 200 |
-
}
|
| 201 |
-
};
|
| 202 |
-
|
| 203 |
-
// App-level concern: JSON export functionality
|
| 204 |
-
const downloadConfigJSON = useCallback(() => {
|
| 205 |
-
if (!calibrationResult) return;
|
| 206 |
-
|
| 207 |
-
const jsonString = JSON.stringify(calibrationResult, null, 2);
|
| 208 |
-
const blob = new Blob([jsonString], { type: "application/json" });
|
| 209 |
-
const url = URL.createObjectURL(blob);
|
| 210 |
-
|
| 211 |
-
const link = document.createElement("a");
|
| 212 |
-
link.href = url;
|
| 213 |
-
link.download = `${robot.robotId || robot.robotType}_calibration.json`;
|
| 214 |
-
document.body.appendChild(link);
|
| 215 |
-
link.click();
|
| 216 |
-
document.body.removeChild(link);
|
| 217 |
-
URL.revokeObjectURL(url);
|
| 218 |
-
}, [calibrationResult, robot.robotId, robot.robotType]);
|
| 219 |
-
|
| 220 |
-
return (
|
| 221 |
-
<div className="space-y-4">
|
| 222 |
-
{/* Calibration Status Card */}
|
| 223 |
-
<Card>
|
| 224 |
-
<CardHeader>
|
| 225 |
-
<div className="flex items-center justify-between">
|
| 226 |
-
<div>
|
| 227 |
-
<CardTitle className="text-lg">
|
| 228 |
-
🛠️ Calibrating: {robot.robotId}
|
| 229 |
-
</CardTitle>
|
| 230 |
-
<CardDescription>
|
| 231 |
-
{robot.robotType?.replace("_", " ")} • {robot.name}
|
| 232 |
-
</CardDescription>
|
| 233 |
-
</div>
|
| 234 |
-
<Badge
|
| 235 |
-
variant={
|
| 236 |
-
isCalibrating
|
| 237 |
-
? "default"
|
| 238 |
-
: calibrationResult
|
| 239 |
-
? "default"
|
| 240 |
-
: "outline"
|
| 241 |
-
}
|
| 242 |
-
>
|
| 243 |
-
{isCalibrating
|
| 244 |
-
? "Recording"
|
| 245 |
-
: calibrationResult
|
| 246 |
-
? "Complete"
|
| 247 |
-
: "Ready"}
|
| 248 |
-
</Badge>
|
| 249 |
-
</div>
|
| 250 |
-
</CardHeader>
|
| 251 |
-
<CardContent>
|
| 252 |
-
<div className="space-y-4">
|
| 253 |
-
<div className="p-3 bg-blue-50 rounded-lg">
|
| 254 |
-
<p className="text-sm font-medium text-blue-900">Status:</p>
|
| 255 |
-
<p className="text-sm text-blue-800">{status}</p>
|
| 256 |
-
{isCalibrating && (
|
| 257 |
-
<p className="text-xs text-blue-600 mt-1">
|
| 258 |
-
Move joints through full range | Press "Finish Recording" or
|
| 259 |
-
Enter key when done
|
| 260 |
-
</p>
|
| 261 |
-
)}
|
| 262 |
-
</div>
|
| 263 |
-
|
| 264 |
-
<div className="flex gap-2">
|
| 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);
|
| 272 |
-
}}
|
| 273 |
-
disabled={isPreparing}
|
| 274 |
-
>
|
| 275 |
-
{isPreparing ? "Preparing..." : "Start Calibration"}
|
| 276 |
-
</Button>
|
| 277 |
-
)}
|
| 278 |
-
|
| 279 |
-
{isCalibrating && calibrationProcess && (
|
| 280 |
-
<Button onClick={handleStopRecording} variant="default">
|
| 281 |
-
Finish Recording
|
| 282 |
-
</Button>
|
| 283 |
-
)}
|
| 284 |
-
|
| 285 |
-
{calibrationResult && (
|
| 286 |
-
<>
|
| 287 |
-
<Button onClick={downloadConfigJSON} variant="outline">
|
| 288 |
-
Download Config JSON
|
| 289 |
-
</Button>
|
| 290 |
-
<Button onClick={onFinish}>Done</Button>
|
| 291 |
-
</>
|
| 292 |
-
)}
|
| 293 |
-
</div>
|
| 294 |
-
</div>
|
| 295 |
-
</CardContent>
|
| 296 |
-
</Card>
|
| 297 |
-
|
| 298 |
-
{/* Configuration JSON Display */}
|
| 299 |
-
{calibrationResult && (
|
| 300 |
-
<Card>
|
| 301 |
-
<CardHeader>
|
| 302 |
-
<CardTitle className="text-lg">
|
| 303 |
-
🎯 Calibration Configuration
|
| 304 |
-
</CardTitle>
|
| 305 |
-
<CardDescription>
|
| 306 |
-
Copy this JSON or download it for your robot setup
|
| 307 |
-
</CardDescription>
|
| 308 |
-
</CardHeader>
|
| 309 |
-
<CardContent>
|
| 310 |
-
<div className="space-y-3">
|
| 311 |
-
<pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
|
| 312 |
-
<code>{JSON.stringify(calibrationResult, null, 2)}</code>
|
| 313 |
-
</pre>
|
| 314 |
-
<div className="flex gap-2">
|
| 315 |
-
<Button onClick={downloadConfigJSON} variant="outline">
|
| 316 |
-
📄 Download JSON File
|
| 317 |
-
</Button>
|
| 318 |
-
<Button
|
| 319 |
-
onClick={() => {
|
| 320 |
-
navigator.clipboard.writeText(
|
| 321 |
-
JSON.stringify(calibrationResult, null, 2)
|
| 322 |
-
);
|
| 323 |
-
}}
|
| 324 |
-
variant="outline"
|
| 325 |
-
>
|
| 326 |
-
📋 Copy to Clipboard
|
| 327 |
-
</Button>
|
| 328 |
-
</div>
|
| 329 |
-
</div>
|
| 330 |
-
</CardContent>
|
| 331 |
-
</Card>
|
| 332 |
-
)}
|
| 333 |
-
|
| 334 |
-
{/* Live Position Recording Table */}
|
| 335 |
-
<Card>
|
| 336 |
-
<CardHeader>
|
| 337 |
-
<CardTitle className="text-lg">Live Position Recording</CardTitle>
|
| 338 |
-
<CardDescription>
|
| 339 |
-
Real-time motor position feedback during calibration
|
| 340 |
-
</CardDescription>
|
| 341 |
-
</CardHeader>
|
| 342 |
-
<CardContent>
|
| 343 |
-
<div className="overflow-hidden rounded-lg border">
|
| 344 |
-
<table className="w-full font-mono text-sm">
|
| 345 |
-
<thead className="bg-gray-50">
|
| 346 |
-
<tr>
|
| 347 |
-
<th className="px-4 py-2 text-left font-medium text-gray-900">
|
| 348 |
-
Motor Name
|
| 349 |
-
</th>
|
| 350 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
| 351 |
-
Current
|
| 352 |
-
</th>
|
| 353 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
| 354 |
-
Min
|
| 355 |
-
</th>
|
| 356 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
| 357 |
-
Max
|
| 358 |
-
</th>
|
| 359 |
-
<th className="px-4 py-2 text-right font-medium text-gray-900">
|
| 360 |
-
Range
|
| 361 |
-
</th>
|
| 362 |
-
</tr>
|
| 363 |
-
</thead>
|
| 364 |
-
<tbody className="divide-y divide-gray-200">
|
| 365 |
-
{motorNames.map((motorName) => {
|
| 366 |
-
const motor = motorData[motorName] || {
|
| 367 |
-
current: 2047,
|
| 368 |
-
min: 2047,
|
| 369 |
-
max: 2047,
|
| 370 |
-
range: 0,
|
| 371 |
-
};
|
| 372 |
-
|
| 373 |
-
return (
|
| 374 |
-
<tr key={motorName} className="hover:bg-gray-50">
|
| 375 |
-
<td className="px-4 py-2 font-medium flex items-center gap-2">
|
| 376 |
-
{motorName}
|
| 377 |
-
{motor.range > 100 && (
|
| 378 |
-
<span className="text-green-600 text-xs">✓</span>
|
| 379 |
-
)}
|
| 380 |
-
</td>
|
| 381 |
-
<td className="px-4 py-2 text-right">{motor.current}</td>
|
| 382 |
-
<td className="px-4 py-2 text-right">{motor.min}</td>
|
| 383 |
-
<td className="px-4 py-2 text-right">{motor.max}</td>
|
| 384 |
-
<td className="px-4 py-2 text-right font-medium">
|
| 385 |
-
<span
|
| 386 |
-
className={
|
| 387 |
-
motor.range > 100
|
| 388 |
-
? "text-green-600"
|
| 389 |
-
: "text-gray-500"
|
| 390 |
-
}
|
| 391 |
-
>
|
| 392 |
-
{motor.range}
|
| 393 |
-
</span>
|
| 394 |
-
</td>
|
| 395 |
-
</tr>
|
| 396 |
-
);
|
| 397 |
-
})}
|
| 398 |
-
</tbody>
|
| 399 |
-
</table>
|
| 400 |
-
</div>
|
| 401 |
-
|
| 402 |
-
{isCalibrating && (
|
| 403 |
-
<div className="mt-3 text-center text-sm text-gray-600">
|
| 404 |
-
Move joints through their full range of motion...
|
| 405 |
-
</div>
|
| 406 |
-
)}
|
| 407 |
-
</CardContent>
|
| 408 |
-
</Card>
|
| 409 |
-
|
| 410 |
-
{/* Calibration Modal */}
|
| 411 |
-
<CalibrationModal
|
| 412 |
-
open={modalOpen}
|
| 413 |
-
onOpenChange={setModalOpen}
|
| 414 |
-
deviceType={robot.robotType || "robot"}
|
| 415 |
-
onContinue={handleContinueCalibration}
|
| 416 |
-
/>
|
| 417 |
-
</div>
|
| 418 |
-
);
|
| 419 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ErrorBoundary.tsx
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
import { Component, type ErrorInfo, type ReactNode } from "react";
|
| 2 |
-
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
| 3 |
-
import { Button } from "./ui/button";
|
| 4 |
-
|
| 5 |
-
interface Props {
|
| 6 |
-
children: ReactNode;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
interface State {
|
| 10 |
-
hasError: boolean;
|
| 11 |
-
error?: Error;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
export class ErrorBoundary extends Component<Props, State> {
|
| 15 |
-
constructor(props: Props) {
|
| 16 |
-
super(props);
|
| 17 |
-
this.state = { hasError: false };
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
static getDerivedStateFromError(error: Error): State {
|
| 21 |
-
return { hasError: true, error };
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
| 25 |
-
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
render() {
|
| 29 |
-
if (this.state.hasError) {
|
| 30 |
-
return (
|
| 31 |
-
<div className="min-h-screen flex items-center justify-center p-8">
|
| 32 |
-
<div className="max-w-md w-full">
|
| 33 |
-
<Alert variant="destructive">
|
| 34 |
-
<AlertTitle>Something went wrong</AlertTitle>
|
| 35 |
-
<AlertDescription>
|
| 36 |
-
The application encountered an error. Please try refreshing the
|
| 37 |
-
page or contact support if the problem persists.
|
| 38 |
-
</AlertDescription>
|
| 39 |
-
</Alert>
|
| 40 |
-
<div className="mt-4 flex gap-2">
|
| 41 |
-
<Button onClick={() => window.location.reload()}>
|
| 42 |
-
Refresh Page
|
| 43 |
-
</Button>
|
| 44 |
-
<Button
|
| 45 |
-
variant="outline"
|
| 46 |
-
onClick={() =>
|
| 47 |
-
this.setState({ hasError: false, error: undefined })
|
| 48 |
-
}
|
| 49 |
-
>
|
| 50 |
-
Try Again
|
| 51 |
-
</Button>
|
| 52 |
-
</div>
|
| 53 |
-
{process.env.NODE_ENV === "development" && this.state.error && (
|
| 54 |
-
<div className="mt-4 p-4 bg-gray-100 rounded-md text-xs">
|
| 55 |
-
<pre>{this.state.error.stack}</pre>
|
| 56 |
-
</div>
|
| 57 |
-
)}
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
);
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
return this.props.children;
|
| 64 |
-
}
|
| 65 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/PortManager.tsx
DELETED
|
@@ -1,1251 +0,0 @@
|
|
| 1 |
-
import { useState, useEffect } from "react";
|
| 2 |
-
import { Button } from "./ui/button";
|
| 3 |
-
import {
|
| 4 |
-
Card,
|
| 5 |
-
CardContent,
|
| 6 |
-
CardDescription,
|
| 7 |
-
CardHeader,
|
| 8 |
-
CardTitle,
|
| 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 |
-
|
| 48 |
-
export function PortManager({
|
| 49 |
-
connectedRobots,
|
| 50 |
-
onConnectedRobotsChange,
|
| 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");
|
| 458 |
-
return;
|
| 459 |
-
}
|
| 460 |
-
|
| 461 |
-
try {
|
| 462 |
-
setIsFindingPorts(true);
|
| 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");
|
| 507 |
-
} finally {
|
| 508 |
-
setIsFindingPorts(false);
|
| 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 |
-
|
| 563 |
-
return (
|
| 564 |
-
<Card>
|
| 565 |
-
<CardHeader>
|
| 566 |
-
<CardTitle>🔌 Robot Connection Manager</CardTitle>
|
| 567 |
-
<CardDescription>
|
| 568 |
-
Connect, identify, and manage your robot arms
|
| 569 |
-
</CardDescription>
|
| 570 |
-
</CardHeader>
|
| 571 |
-
<CardContent>
|
| 572 |
-
<div className="space-y-6">
|
| 573 |
-
{/* Error Display */}
|
| 574 |
-
{error && (
|
| 575 |
-
<Alert variant="destructive">
|
| 576 |
-
<AlertDescription>{error}</AlertDescription>
|
| 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}
|
| 605 |
-
</div>
|
| 606 |
-
))}
|
| 607 |
-
</div>
|
| 608 |
-
)}
|
| 609 |
-
|
| 610 |
-
{/* Connected Ports */}
|
| 611 |
-
<div>
|
| 612 |
-
<h4 className="font-semibold mb-3">
|
| 613 |
-
Connected Robots ({connectedRobots.length})
|
| 614 |
-
</h4>
|
| 615 |
-
|
| 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>
|
| 639 |
-
)}
|
| 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"
|
| 1037 |
-
size="sm"
|
| 1038 |
-
onClick={() => setIsEditing(true)}
|
| 1039 |
-
>
|
| 1040 |
-
Edit
|
| 1041 |
-
</Button>
|
| 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
|
| 1050 |
-
</div>
|
| 1051 |
-
<Button
|
| 1052 |
-
variant="outline"
|
| 1053 |
-
size="sm"
|
| 1054 |
-
onClick={() => setIsEditing(true)}
|
| 1055 |
-
>
|
| 1056 |
-
Configure
|
| 1057 |
-
</Button>
|
| 1058 |
-
</div>
|
| 1059 |
-
)}
|
| 1060 |
-
|
| 1061 |
-
{/* Robot Configuration Form (when editing) */}
|
| 1062 |
-
{isEditing && (
|
| 1063 |
-
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
|
| 1064 |
-
<div className="grid grid-cols-2 gap-3">
|
| 1065 |
-
<div>
|
| 1066 |
-
<label className="text-sm font-medium block mb-1">
|
| 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 |
-
}
|
| 1076 |
-
className="w-full px-2 py-1 border rounded text-sm"
|
| 1077 |
-
>
|
| 1078 |
-
<option value="so100_follower">SO-100 Follower</option>
|
| 1079 |
-
<option value="so100_leader">SO-100 Leader</option>
|
| 1080 |
-
</select>
|
| 1081 |
-
</div>
|
| 1082 |
-
<div>
|
| 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 |
-
/>
|
| 1091 |
-
</div>
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/TeleoperationPanel.tsx
DELETED
|
@@ -1,530 +0,0 @@
|
|
| 1 |
-
import { useState, useEffect, useRef, useCallback } from "react";
|
| 2 |
-
import { Button } from "./ui/button";
|
| 3 |
-
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
| 4 |
-
import { Badge } from "./ui/badge";
|
| 5 |
-
import { Alert, AlertDescription } from "./ui/alert";
|
| 6 |
-
import {
|
| 7 |
-
teleoperate,
|
| 8 |
-
type TeleoperationProcess,
|
| 9 |
-
type TeleoperationState,
|
| 10 |
-
} from "@lerobot/web";
|
| 11 |
-
import { getUnifiedRobotData } from "../lib/unified-storage";
|
| 12 |
-
import type { RobotConnection } from "@lerobot/web";
|
| 13 |
-
import { SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
|
| 14 |
-
|
| 15 |
-
interface TeleoperationPanelProps {
|
| 16 |
-
robot: RobotConnection;
|
| 17 |
-
onClose: () => void;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
export function TeleoperationPanel({
|
| 21 |
-
robot,
|
| 22 |
-
onClose,
|
| 23 |
-
}: TeleoperationPanelProps) {
|
| 24 |
-
const [teleoperationState, setTeleoperationState] =
|
| 25 |
-
useState<TeleoperationState>({
|
| 26 |
-
isActive: false,
|
| 27 |
-
motorConfigs: [],
|
| 28 |
-
lastUpdate: 0,
|
| 29 |
-
keyStates: {},
|
| 30 |
-
});
|
| 31 |
-
const [error, setError] = useState<string | null>(null);
|
| 32 |
-
const [, setIsInitialized] = useState(false);
|
| 33 |
-
|
| 34 |
-
const teleoperationProcessRef = useRef<TeleoperationProcess | null>(null);
|
| 35 |
-
|
| 36 |
-
// Initialize teleoperation process
|
| 37 |
-
useEffect(() => {
|
| 38 |
-
const initializeTeleoperation = async () => {
|
| 39 |
-
if (!robot || !robot.robotType) {
|
| 40 |
-
setError("No robot configuration available");
|
| 41 |
-
return;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
try {
|
| 45 |
-
// Load calibration data from demo storage (app concern)
|
| 46 |
-
let calibrationData;
|
| 47 |
-
if (robot.serialNumber) {
|
| 48 |
-
const data = getUnifiedRobotData(robot.serialNumber);
|
| 49 |
-
calibrationData = data?.calibration;
|
| 50 |
-
if (calibrationData) {
|
| 51 |
-
console.log("✅ Loaded calibration data for", robot.serialNumber);
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
// Create teleoperation process using clean library API
|
| 56 |
-
const process = await teleoperate(robot, {
|
| 57 |
-
calibrationData,
|
| 58 |
-
onStateUpdate: (state: TeleoperationState) => {
|
| 59 |
-
setTeleoperationState(state);
|
| 60 |
-
},
|
| 61 |
-
});
|
| 62 |
-
|
| 63 |
-
teleoperationProcessRef.current = process;
|
| 64 |
-
setTeleoperationState(process.getState());
|
| 65 |
-
setIsInitialized(true);
|
| 66 |
-
setError(null);
|
| 67 |
-
} catch (error) {
|
| 68 |
-
const errorMessage =
|
| 69 |
-
error instanceof Error
|
| 70 |
-
? error.message
|
| 71 |
-
: "Failed to initialize teleoperation";
|
| 72 |
-
setError(errorMessage);
|
| 73 |
-
console.error("❌ Failed to initialize teleoperation:", error);
|
| 74 |
-
}
|
| 75 |
-
};
|
| 76 |
-
|
| 77 |
-
initializeTeleoperation();
|
| 78 |
-
|
| 79 |
-
return () => {
|
| 80 |
-
// Cleanup on unmount
|
| 81 |
-
if (teleoperationProcessRef.current) {
|
| 82 |
-
teleoperationProcessRef.current.disconnect();
|
| 83 |
-
teleoperationProcessRef.current = null;
|
| 84 |
-
}
|
| 85 |
-
};
|
| 86 |
-
}, [robot]);
|
| 87 |
-
|
| 88 |
-
// Keyboard event handlers
|
| 89 |
-
const handleKeyDown = useCallback(
|
| 90 |
-
(event: KeyboardEvent) => {
|
| 91 |
-
if (!teleoperationState.isActive || !teleoperationProcessRef.current)
|
| 92 |
-
return;
|
| 93 |
-
|
| 94 |
-
const key = event.key;
|
| 95 |
-
event.preventDefault();
|
| 96 |
-
teleoperationProcessRef.current.updateKeyState(key, true);
|
| 97 |
-
},
|
| 98 |
-
[teleoperationState.isActive]
|
| 99 |
-
);
|
| 100 |
-
|
| 101 |
-
const handleKeyUp = useCallback(
|
| 102 |
-
(event: KeyboardEvent) => {
|
| 103 |
-
if (!teleoperationState.isActive || !teleoperationProcessRef.current)
|
| 104 |
-
return;
|
| 105 |
-
|
| 106 |
-
const key = event.key;
|
| 107 |
-
event.preventDefault();
|
| 108 |
-
teleoperationProcessRef.current.updateKeyState(key, false);
|
| 109 |
-
},
|
| 110 |
-
[teleoperationState.isActive]
|
| 111 |
-
);
|
| 112 |
-
|
| 113 |
-
// Register keyboard events
|
| 114 |
-
useEffect(() => {
|
| 115 |
-
if (teleoperationState.isActive) {
|
| 116 |
-
window.addEventListener("keydown", handleKeyDown);
|
| 117 |
-
window.addEventListener("keyup", handleKeyUp);
|
| 118 |
-
|
| 119 |
-
return () => {
|
| 120 |
-
window.removeEventListener("keydown", handleKeyDown);
|
| 121 |
-
window.removeEventListener("keyup", handleKeyUp);
|
| 122 |
-
};
|
| 123 |
-
}
|
| 124 |
-
}, [teleoperationState.isActive, handleKeyDown, handleKeyUp]);
|
| 125 |
-
|
| 126 |
-
const handleStart = () => {
|
| 127 |
-
if (!teleoperationProcessRef.current) {
|
| 128 |
-
setError("Teleoperation not initialized");
|
| 129 |
-
return;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
try {
|
| 133 |
-
teleoperationProcessRef.current.start();
|
| 134 |
-
console.log("🎮 Teleoperation started");
|
| 135 |
-
} catch (error) {
|
| 136 |
-
const errorMessage =
|
| 137 |
-
error instanceof Error
|
| 138 |
-
? error.message
|
| 139 |
-
: "Failed to start teleoperation";
|
| 140 |
-
setError(errorMessage);
|
| 141 |
-
}
|
| 142 |
-
};
|
| 143 |
-
|
| 144 |
-
const handleStop = () => {
|
| 145 |
-
if (!teleoperationProcessRef.current) return;
|
| 146 |
-
|
| 147 |
-
teleoperationProcessRef.current.stop();
|
| 148 |
-
console.log("🛑 Teleoperation stopped");
|
| 149 |
-
};
|
| 150 |
-
|
| 151 |
-
const handleClose = () => {
|
| 152 |
-
if (teleoperationProcessRef.current) {
|
| 153 |
-
teleoperationProcessRef.current.stop();
|
| 154 |
-
}
|
| 155 |
-
onClose();
|
| 156 |
-
};
|
| 157 |
-
|
| 158 |
-
const simulateKeyPress = (key: string) => {
|
| 159 |
-
if (!teleoperationProcessRef.current) return;
|
| 160 |
-
teleoperationProcessRef.current.updateKeyState(key, true);
|
| 161 |
-
};
|
| 162 |
-
|
| 163 |
-
const simulateKeyRelease = (key: string) => {
|
| 164 |
-
if (!teleoperationProcessRef.current) return;
|
| 165 |
-
teleoperationProcessRef.current.updateKeyState(key, false);
|
| 166 |
-
};
|
| 167 |
-
|
| 168 |
-
const moveMotorToPosition = async (motorIndex: number, position: number) => {
|
| 169 |
-
if (!teleoperationProcessRef.current) return;
|
| 170 |
-
|
| 171 |
-
try {
|
| 172 |
-
const motorName = teleoperationState.motorConfigs[motorIndex]?.name;
|
| 173 |
-
if (motorName) {
|
| 174 |
-
await teleoperationProcessRef.current.moveMotor(motorName, position);
|
| 175 |
-
}
|
| 176 |
-
} catch (error) {
|
| 177 |
-
console.warn(
|
| 178 |
-
`Failed to move motor ${motorIndex + 1} to position ${position}:`,
|
| 179 |
-
error
|
| 180 |
-
);
|
| 181 |
-
}
|
| 182 |
-
};
|
| 183 |
-
|
| 184 |
-
const isConnected = robot?.isConnected || false;
|
| 185 |
-
const isActive = teleoperationState.isActive;
|
| 186 |
-
const motorConfigs = teleoperationState.motorConfigs;
|
| 187 |
-
const keyStates = teleoperationState.keyStates;
|
| 188 |
-
|
| 189 |
-
// Virtual keyboard component
|
| 190 |
-
const VirtualKeyboard = () => {
|
| 191 |
-
const isKeyPressed = (key: string) => {
|
| 192 |
-
return keyStates[key]?.pressed || false;
|
| 193 |
-
};
|
| 194 |
-
|
| 195 |
-
const KeyButton = ({
|
| 196 |
-
keyCode,
|
| 197 |
-
children,
|
| 198 |
-
className = "",
|
| 199 |
-
size = "default" as "default" | "sm" | "lg" | "icon",
|
| 200 |
-
}: {
|
| 201 |
-
keyCode: string;
|
| 202 |
-
children: React.ReactNode;
|
| 203 |
-
className?: string;
|
| 204 |
-
size?: "default" | "sm" | "lg" | "icon";
|
| 205 |
-
}) => {
|
| 206 |
-
const control =
|
| 207 |
-
SO100_KEYBOARD_CONTROLS[
|
| 208 |
-
keyCode as keyof typeof SO100_KEYBOARD_CONTROLS
|
| 209 |
-
];
|
| 210 |
-
const pressed = isKeyPressed(keyCode);
|
| 211 |
-
|
| 212 |
-
return (
|
| 213 |
-
<Button
|
| 214 |
-
variant={pressed ? "default" : "outline"}
|
| 215 |
-
size={size}
|
| 216 |
-
className={`
|
| 217 |
-
${className}
|
| 218 |
-
${
|
| 219 |
-
pressed
|
| 220 |
-
? "bg-blue-600 text-white shadow-inner"
|
| 221 |
-
: "hover:bg-gray-100"
|
| 222 |
-
}
|
| 223 |
-
transition-all duration-75 font-mono text-xs
|
| 224 |
-
${!isActive ? "opacity-50 cursor-not-allowed" : ""}
|
| 225 |
-
`}
|
| 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 |
-
>
|
| 241 |
-
{children}
|
| 242 |
-
</Button>
|
| 243 |
-
);
|
| 244 |
-
};
|
| 245 |
-
|
| 246 |
-
return (
|
| 247 |
-
<div className="space-y-4">
|
| 248 |
-
{/* Arrow Keys */}
|
| 249 |
-
<div className="text-center">
|
| 250 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">Shoulder</h4>
|
| 251 |
-
<div className="flex flex-col items-center gap-1">
|
| 252 |
-
<KeyButton keyCode="ArrowUp" size="sm">
|
| 253 |
-
↑
|
| 254 |
-
</KeyButton>
|
| 255 |
-
<div className="flex gap-1">
|
| 256 |
-
<KeyButton keyCode="ArrowLeft" size="sm">
|
| 257 |
-
←
|
| 258 |
-
</KeyButton>
|
| 259 |
-
<KeyButton keyCode="ArrowDown" size="sm">
|
| 260 |
-
↓
|
| 261 |
-
</KeyButton>
|
| 262 |
-
<KeyButton keyCode="ArrowRight" size="sm">
|
| 263 |
-
→
|
| 264 |
-
</KeyButton>
|
| 265 |
-
</div>
|
| 266 |
-
</div>
|
| 267 |
-
</div>
|
| 268 |
-
|
| 269 |
-
{/* WASD Keys */}
|
| 270 |
-
<div className="text-center">
|
| 271 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">
|
| 272 |
-
Elbow/Wrist
|
| 273 |
-
</h4>
|
| 274 |
-
<div className="flex flex-col items-center gap-1">
|
| 275 |
-
<KeyButton keyCode="w" size="sm">
|
| 276 |
-
W
|
| 277 |
-
</KeyButton>
|
| 278 |
-
<div className="flex gap-1">
|
| 279 |
-
<KeyButton keyCode="a" size="sm">
|
| 280 |
-
A
|
| 281 |
-
</KeyButton>
|
| 282 |
-
<KeyButton keyCode="s" size="sm">
|
| 283 |
-
S
|
| 284 |
-
</KeyButton>
|
| 285 |
-
<KeyButton keyCode="d" size="sm">
|
| 286 |
-
D
|
| 287 |
-
</KeyButton>
|
| 288 |
-
</div>
|
| 289 |
-
</div>
|
| 290 |
-
</div>
|
| 291 |
-
|
| 292 |
-
{/* Q/E and Gripper */}
|
| 293 |
-
<div className="flex justify-center gap-2">
|
| 294 |
-
<div className="text-center">
|
| 295 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">Roll</h4>
|
| 296 |
-
<div className="flex gap-1">
|
| 297 |
-
<KeyButton keyCode="q" size="sm">
|
| 298 |
-
Q
|
| 299 |
-
</KeyButton>
|
| 300 |
-
<KeyButton keyCode="e" size="sm">
|
| 301 |
-
E
|
| 302 |
-
</KeyButton>
|
| 303 |
-
</div>
|
| 304 |
-
</div>
|
| 305 |
-
<div className="text-center">
|
| 306 |
-
<h4 className="text-xs font-semibold mb-2 text-gray-600">
|
| 307 |
-
Gripper
|
| 308 |
-
</h4>
|
| 309 |
-
<div className="flex gap-1">
|
| 310 |
-
<KeyButton keyCode="o" size="sm">
|
| 311 |
-
O
|
| 312 |
-
</KeyButton>
|
| 313 |
-
<KeyButton keyCode="c" size="sm">
|
| 314 |
-
C
|
| 315 |
-
</KeyButton>
|
| 316 |
-
</div>
|
| 317 |
-
</div>
|
| 318 |
-
</div>
|
| 319 |
-
|
| 320 |
-
{/* Emergency Stop */}
|
| 321 |
-
<div className="text-center border-t pt-2">
|
| 322 |
-
<KeyButton
|
| 323 |
-
keyCode="Escape"
|
| 324 |
-
className="bg-red-100 border-red-300 hover:bg-red-200 text-red-800 text-xs"
|
| 325 |
-
>
|
| 326 |
-
ESC
|
| 327 |
-
</KeyButton>
|
| 328 |
-
</div>
|
| 329 |
-
</div>
|
| 330 |
-
);
|
| 331 |
-
};
|
| 332 |
-
|
| 333 |
-
return (
|
| 334 |
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
| 335 |
-
<div className="container mx-auto px-6 py-8">
|
| 336 |
-
{/* Header */}
|
| 337 |
-
<div className="flex justify-between items-center mb-6">
|
| 338 |
-
<div>
|
| 339 |
-
<h1 className="text-3xl font-bold text-gray-900">
|
| 340 |
-
🎮 Robot Teleoperation
|
| 341 |
-
</h1>
|
| 342 |
-
<p className="text-gray-600">
|
| 343 |
-
{robot.robotId || robot.name} - {robot.serialNumber}
|
| 344 |
-
</p>
|
| 345 |
-
</div>
|
| 346 |
-
<Button variant="outline" onClick={handleClose}>
|
| 347 |
-
← Back to Dashboard
|
| 348 |
-
</Button>
|
| 349 |
-
</div>
|
| 350 |
-
|
| 351 |
-
{/* Error Alert */}
|
| 352 |
-
{error && (
|
| 353 |
-
<Alert variant="destructive" className="mb-6">
|
| 354 |
-
<AlertDescription>{error}</AlertDescription>
|
| 355 |
-
</Alert>
|
| 356 |
-
)}
|
| 357 |
-
|
| 358 |
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 359 |
-
{/* Status Panel */}
|
| 360 |
-
<Card>
|
| 361 |
-
<CardHeader>
|
| 362 |
-
<CardTitle className="flex items-center gap-2">
|
| 363 |
-
Status
|
| 364 |
-
<Badge variant={isConnected ? "default" : "destructive"}>
|
| 365 |
-
{isConnected ? "Connected" : "Disconnected"}
|
| 366 |
-
</Badge>
|
| 367 |
-
</CardTitle>
|
| 368 |
-
</CardHeader>
|
| 369 |
-
<CardContent className="space-y-4">
|
| 370 |
-
<div className="flex items-center justify-between">
|
| 371 |
-
<span className="text-sm text-gray-600">Teleoperation</span>
|
| 372 |
-
<Badge variant={isActive ? "default" : "secondary"}>
|
| 373 |
-
{isActive ? "Active" : "Stopped"}
|
| 374 |
-
</Badge>
|
| 375 |
-
</div>
|
| 376 |
-
|
| 377 |
-
<div className="flex items-center justify-between">
|
| 378 |
-
<span className="text-sm text-gray-600">Active Keys</span>
|
| 379 |
-
<Badge variant="outline">
|
| 380 |
-
{
|
| 381 |
-
Object.values(keyStates).filter((state) => state.pressed)
|
| 382 |
-
.length
|
| 383 |
-
}
|
| 384 |
-
</Badge>
|
| 385 |
-
</div>
|
| 386 |
-
|
| 387 |
-
<div className="space-y-2">
|
| 388 |
-
{isActive ? (
|
| 389 |
-
<Button
|
| 390 |
-
onClick={handleStop}
|
| 391 |
-
variant="destructive"
|
| 392 |
-
className="w-full"
|
| 393 |
-
>
|
| 394 |
-
⏹️ Stop Teleoperation
|
| 395 |
-
</Button>
|
| 396 |
-
) : (
|
| 397 |
-
<Button
|
| 398 |
-
onClick={handleStart}
|
| 399 |
-
disabled={!isConnected}
|
| 400 |
-
className="w-full"
|
| 401 |
-
>
|
| 402 |
-
▶️ Start Teleoperation
|
| 403 |
-
</Button>
|
| 404 |
-
)}
|
| 405 |
-
</div>
|
| 406 |
-
</CardContent>
|
| 407 |
-
</Card>
|
| 408 |
-
|
| 409 |
-
{/* Virtual Keyboard */}
|
| 410 |
-
<Card>
|
| 411 |
-
<CardHeader>
|
| 412 |
-
<CardTitle>Virtual Keyboard</CardTitle>
|
| 413 |
-
</CardHeader>
|
| 414 |
-
<CardContent>
|
| 415 |
-
<VirtualKeyboard />
|
| 416 |
-
</CardContent>
|
| 417 |
-
</Card>
|
| 418 |
-
|
| 419 |
-
{/* Motor Status */}
|
| 420 |
-
<Card>
|
| 421 |
-
<CardHeader>
|
| 422 |
-
<CardTitle>Motor Positions</CardTitle>
|
| 423 |
-
</CardHeader>
|
| 424 |
-
<CardContent className="space-y-3">
|
| 425 |
-
{motorConfigs.map((motor, index) => {
|
| 426 |
-
return (
|
| 427 |
-
<div key={motor.name} className="space-y-1">
|
| 428 |
-
<div className="flex justify-between items-center">
|
| 429 |
-
<span className="text-sm font-medium">
|
| 430 |
-
{motor.name.replace("_", " ")}
|
| 431 |
-
</span>
|
| 432 |
-
<span className="text-xs text-gray-500">
|
| 433 |
-
{motor.currentPosition}
|
| 434 |
-
</span>
|
| 435 |
-
</div>
|
| 436 |
-
<input
|
| 437 |
-
type="range"
|
| 438 |
-
min={motor.minPosition}
|
| 439 |
-
max={motor.maxPosition}
|
| 440 |
-
value={motor.currentPosition}
|
| 441 |
-
disabled={!isActive}
|
| 442 |
-
className={`w-full h-2 rounded-lg appearance-none cursor-pointer bg-gray-200 slider-thumb ${
|
| 443 |
-
!isActive ? "opacity-50 cursor-not-allowed" : ""
|
| 444 |
-
}`}
|
| 445 |
-
style={{
|
| 446 |
-
background: isActive
|
| 447 |
-
? `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${
|
| 448 |
-
((motor.currentPosition - motor.minPosition) /
|
| 449 |
-
(motor.maxPosition - motor.minPosition)) *
|
| 450 |
-
100
|
| 451 |
-
}%, #e5e7eb ${
|
| 452 |
-
((motor.currentPosition - motor.minPosition) /
|
| 453 |
-
(motor.maxPosition - motor.minPosition)) *
|
| 454 |
-
100
|
| 455 |
-
}%, #e5e7eb 100%)`
|
| 456 |
-
: "#e5e7eb",
|
| 457 |
-
}}
|
| 458 |
-
onChange={async (e) => {
|
| 459 |
-
if (!isActive) return;
|
| 460 |
-
const newPosition = parseInt(e.target.value);
|
| 461 |
-
try {
|
| 462 |
-
await moveMotorToPosition(index, newPosition);
|
| 463 |
-
} catch (error) {
|
| 464 |
-
console.warn(
|
| 465 |
-
"Failed to move motor via slider:",
|
| 466 |
-
error
|
| 467 |
-
);
|
| 468 |
-
}
|
| 469 |
-
}}
|
| 470 |
-
/>
|
| 471 |
-
<div className="flex justify-between text-xs text-gray-400">
|
| 472 |
-
<span>{motor.minPosition}</span>
|
| 473 |
-
<span>{motor.maxPosition}</span>
|
| 474 |
-
</div>
|
| 475 |
-
</div>
|
| 476 |
-
);
|
| 477 |
-
})}
|
| 478 |
-
</CardContent>
|
| 479 |
-
</Card>
|
| 480 |
-
</div>
|
| 481 |
-
|
| 482 |
-
{/* Help Card */}
|
| 483 |
-
<Card className="mt-6">
|
| 484 |
-
<CardHeader>
|
| 485 |
-
<CardTitle>Control Instructions</CardTitle>
|
| 486 |
-
</CardHeader>
|
| 487 |
-
<CardContent>
|
| 488 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
| 489 |
-
<div>
|
| 490 |
-
<h4 className="font-semibold mb-2">Arrow Keys</h4>
|
| 491 |
-
<ul className="space-y-1 text-gray-600">
|
| 492 |
-
<li>↑ ↓ Shoulder lift</li>
|
| 493 |
-
<li>← → Shoulder pan</li>
|
| 494 |
-
</ul>
|
| 495 |
-
</div>
|
| 496 |
-
<div>
|
| 497 |
-
<h4 className="font-semibold mb-2">WASD Keys</h4>
|
| 498 |
-
<ul className="space-y-1 text-gray-600">
|
| 499 |
-
<li>W S Elbow flex</li>
|
| 500 |
-
<li>A D Wrist flex</li>
|
| 501 |
-
</ul>
|
| 502 |
-
</div>
|
| 503 |
-
<div>
|
| 504 |
-
<h4 className="font-semibold mb-2">Other Keys</h4>
|
| 505 |
-
<ul className="space-y-1 text-gray-600">
|
| 506 |
-
<li>Q E Wrist roll</li>
|
| 507 |
-
<li>O Open gripper</li>
|
| 508 |
-
<li>C Close gripper</li>
|
| 509 |
-
</ul>
|
| 510 |
-
</div>
|
| 511 |
-
<div>
|
| 512 |
-
<h4 className="font-semibold mb-2 text-red-700">Emergency</h4>
|
| 513 |
-
<ul className="space-y-1 text-red-600">
|
| 514 |
-
<li>ESC Emergency stop</li>
|
| 515 |
-
</ul>
|
| 516 |
-
</div>
|
| 517 |
-
</div>
|
| 518 |
-
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
| 519 |
-
<p className="text-sm text-blue-800">
|
| 520 |
-
💡 <strong>Pro tip:</strong> Use your physical keyboard for
|
| 521 |
-
faster control, or click the virtual keys below. Hold keys down
|
| 522 |
-
for continuous movement.
|
| 523 |
-
</p>
|
| 524 |
-
</div>
|
| 525 |
-
</CardContent>
|
| 526 |
-
</Card>
|
| 527 |
-
</div>
|
| 528 |
-
</div>
|
| 529 |
-
);
|
| 530 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ui/alert.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
-
import { cn } from "../../lib/utils";
|
| 4 |
-
|
| 5 |
-
const alertVariants = cva(
|
| 6 |
-
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
| 7 |
-
{
|
| 8 |
-
variants: {
|
| 9 |
-
variant: {
|
| 10 |
-
default: "bg-background text-foreground",
|
| 11 |
-
destructive:
|
| 12 |
-
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
| 13 |
-
},
|
| 14 |
-
},
|
| 15 |
-
defaultVariants: {
|
| 16 |
-
variant: "default",
|
| 17 |
-
},
|
| 18 |
-
}
|
| 19 |
-
);
|
| 20 |
-
|
| 21 |
-
const Alert = React.forwardRef<
|
| 22 |
-
HTMLDivElement,
|
| 23 |
-
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
| 24 |
-
>(({ className, variant, ...props }, ref) => (
|
| 25 |
-
<div
|
| 26 |
-
ref={ref}
|
| 27 |
-
role="alert"
|
| 28 |
-
className={cn(alertVariants({ variant }), className)}
|
| 29 |
-
{...props}
|
| 30 |
-
/>
|
| 31 |
-
));
|
| 32 |
-
Alert.displayName = "Alert";
|
| 33 |
-
|
| 34 |
-
const AlertTitle = React.forwardRef<
|
| 35 |
-
HTMLParagraphElement,
|
| 36 |
-
React.HTMLAttributes<HTMLHeadingElement>
|
| 37 |
-
>(({ className, ...props }, ref) => (
|
| 38 |
-
<h5
|
| 39 |
-
ref={ref}
|
| 40 |
-
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
| 41 |
-
{...props}
|
| 42 |
-
/>
|
| 43 |
-
));
|
| 44 |
-
AlertTitle.displayName = "AlertTitle";
|
| 45 |
-
|
| 46 |
-
const AlertDescription = React.forwardRef<
|
| 47 |
-
HTMLParagraphElement,
|
| 48 |
-
React.HTMLAttributes<HTMLParagraphElement>
|
| 49 |
-
>(({ className, ...props }, ref) => (
|
| 50 |
-
<div
|
| 51 |
-
ref={ref}
|
| 52 |
-
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
| 53 |
-
{...props}
|
| 54 |
-
/>
|
| 55 |
-
));
|
| 56 |
-
AlertDescription.displayName = "AlertDescription";
|
| 57 |
-
|
| 58 |
-
export { Alert, AlertTitle, AlertDescription };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ui/badge.tsx
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
-
import { cn } from "../../lib/utils";
|
| 4 |
-
|
| 5 |
-
const badgeVariants = cva(
|
| 6 |
-
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
| 7 |
-
{
|
| 8 |
-
variants: {
|
| 9 |
-
variant: {
|
| 10 |
-
default:
|
| 11 |
-
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
| 12 |
-
secondary:
|
| 13 |
-
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 14 |
-
destructive:
|
| 15 |
-
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
| 16 |
-
outline: "text-foreground",
|
| 17 |
-
},
|
| 18 |
-
},
|
| 19 |
-
defaultVariants: {
|
| 20 |
-
variant: "default",
|
| 21 |
-
},
|
| 22 |
-
}
|
| 23 |
-
);
|
| 24 |
-
|
| 25 |
-
export interface BadgeProps
|
| 26 |
-
extends React.HTMLAttributes<HTMLDivElement>,
|
| 27 |
-
VariantProps<typeof badgeVariants> {}
|
| 28 |
-
|
| 29 |
-
function Badge({ className, variant, ...props }: BadgeProps) {
|
| 30 |
-
return (
|
| 31 |
-
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
| 32 |
-
);
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
export { Badge, badgeVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ui/button.tsx
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
-
import { cn } from "../../lib/utils";
|
| 4 |
-
|
| 5 |
-
const buttonVariants = cva(
|
| 6 |
-
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
| 7 |
-
{
|
| 8 |
-
variants: {
|
| 9 |
-
variant: {
|
| 10 |
-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
| 11 |
-
destructive:
|
| 12 |
-
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
| 13 |
-
outline:
|
| 14 |
-
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
| 15 |
-
secondary:
|
| 16 |
-
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 17 |
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 18 |
-
link: "text-primary underline-offset-4 hover:underline",
|
| 19 |
-
},
|
| 20 |
-
size: {
|
| 21 |
-
default: "h-10 px-4 py-2",
|
| 22 |
-
sm: "h-9 rounded-md px-3",
|
| 23 |
-
lg: "h-11 rounded-md px-8",
|
| 24 |
-
icon: "h-10 w-10",
|
| 25 |
-
},
|
| 26 |
-
},
|
| 27 |
-
defaultVariants: {
|
| 28 |
-
variant: "default",
|
| 29 |
-
size: "default",
|
| 30 |
-
},
|
| 31 |
-
}
|
| 32 |
-
);
|
| 33 |
-
|
| 34 |
-
export interface ButtonProps
|
| 35 |
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
| 36 |
-
VariantProps<typeof buttonVariants> {
|
| 37 |
-
asChild?: boolean;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 41 |
-
({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 42 |
-
return (
|
| 43 |
-
<button
|
| 44 |
-
className={cn(buttonVariants({ variant, size, className }))}
|
| 45 |
-
ref={ref}
|
| 46 |
-
{...props}
|
| 47 |
-
/>
|
| 48 |
-
);
|
| 49 |
-
}
|
| 50 |
-
);
|
| 51 |
-
Button.displayName = "Button";
|
| 52 |
-
|
| 53 |
-
export { Button, buttonVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ui/card.tsx
DELETED
|
@@ -1,85 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import { cn } from "../../lib/utils";
|
| 3 |
-
|
| 4 |
-
const Card = React.forwardRef<
|
| 5 |
-
HTMLDivElement,
|
| 6 |
-
React.HTMLAttributes<HTMLDivElement>
|
| 7 |
-
>(({ className, ...props }, ref) => (
|
| 8 |
-
<div
|
| 9 |
-
ref={ref}
|
| 10 |
-
className={cn(
|
| 11 |
-
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
| 12 |
-
className
|
| 13 |
-
)}
|
| 14 |
-
{...props}
|
| 15 |
-
/>
|
| 16 |
-
));
|
| 17 |
-
Card.displayName = "Card";
|
| 18 |
-
|
| 19 |
-
const CardHeader = React.forwardRef<
|
| 20 |
-
HTMLDivElement,
|
| 21 |
-
React.HTMLAttributes<HTMLDivElement>
|
| 22 |
-
>(({ className, ...props }, ref) => (
|
| 23 |
-
<div
|
| 24 |
-
ref={ref}
|
| 25 |
-
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
| 26 |
-
{...props}
|
| 27 |
-
/>
|
| 28 |
-
));
|
| 29 |
-
CardHeader.displayName = "CardHeader";
|
| 30 |
-
|
| 31 |
-
const CardTitle = React.forwardRef<
|
| 32 |
-
HTMLParagraphElement,
|
| 33 |
-
React.HTMLAttributes<HTMLHeadingElement>
|
| 34 |
-
>(({ className, ...props }, ref) => (
|
| 35 |
-
<h3
|
| 36 |
-
ref={ref}
|
| 37 |
-
className={cn(
|
| 38 |
-
"text-2xl font-semibold leading-none tracking-tight",
|
| 39 |
-
className
|
| 40 |
-
)}
|
| 41 |
-
{...props}
|
| 42 |
-
/>
|
| 43 |
-
));
|
| 44 |
-
CardTitle.displayName = "CardTitle";
|
| 45 |
-
|
| 46 |
-
const CardDescription = React.forwardRef<
|
| 47 |
-
HTMLParagraphElement,
|
| 48 |
-
React.HTMLAttributes<HTMLParagraphElement>
|
| 49 |
-
>(({ className, ...props }, ref) => (
|
| 50 |
-
<p
|
| 51 |
-
ref={ref}
|
| 52 |
-
className={cn("text-sm text-muted-foreground", className)}
|
| 53 |
-
{...props}
|
| 54 |
-
/>
|
| 55 |
-
));
|
| 56 |
-
CardDescription.displayName = "CardDescription";
|
| 57 |
-
|
| 58 |
-
const CardContent = React.forwardRef<
|
| 59 |
-
HTMLDivElement,
|
| 60 |
-
React.HTMLAttributes<HTMLDivElement>
|
| 61 |
-
>(({ className, ...props }, ref) => (
|
| 62 |
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
| 63 |
-
));
|
| 64 |
-
CardContent.displayName = "CardContent";
|
| 65 |
-
|
| 66 |
-
const CardFooter = React.forwardRef<
|
| 67 |
-
HTMLDivElement,
|
| 68 |
-
React.HTMLAttributes<HTMLDivElement>
|
| 69 |
-
>(({ className, ...props }, ref) => (
|
| 70 |
-
<div
|
| 71 |
-
ref={ref}
|
| 72 |
-
className={cn("flex items-center p-6 pt-0", className)}
|
| 73 |
-
{...props}
|
| 74 |
-
/>
|
| 75 |
-
));
|
| 76 |
-
CardFooter.displayName = "CardFooter";
|
| 77 |
-
|
| 78 |
-
export {
|
| 79 |
-
Card,
|
| 80 |
-
CardHeader,
|
| 81 |
-
CardFooter,
|
| 82 |
-
CardTitle,
|
| 83 |
-
CardDescription,
|
| 84 |
-
CardContent,
|
| 85 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ui/dialog.tsx
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
| 3 |
-
import { X } from "lucide-react";
|
| 4 |
-
|
| 5 |
-
import { cn } from "../../lib/utils";
|
| 6 |
-
|
| 7 |
-
const Dialog = DialogPrimitive.Root;
|
| 8 |
-
|
| 9 |
-
const DialogTrigger = DialogPrimitive.Trigger;
|
| 10 |
-
|
| 11 |
-
const DialogPortal = DialogPrimitive.Portal;
|
| 12 |
-
|
| 13 |
-
const DialogClose = DialogPrimitive.Close;
|
| 14 |
-
|
| 15 |
-
const DialogOverlay = React.forwardRef<
|
| 16 |
-
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
| 17 |
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
| 18 |
-
>(({ className, ...props }, ref) => (
|
| 19 |
-
<DialogPrimitive.Overlay
|
| 20 |
-
ref={ref}
|
| 21 |
-
className={cn(
|
| 22 |
-
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
| 23 |
-
className
|
| 24 |
-
)}
|
| 25 |
-
{...props}
|
| 26 |
-
/>
|
| 27 |
-
));
|
| 28 |
-
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
| 29 |
-
|
| 30 |
-
const DialogContent = React.forwardRef<
|
| 31 |
-
React.ElementRef<typeof DialogPrimitive.Content>,
|
| 32 |
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
| 33 |
-
>(({ className, children, ...props }, ref) => (
|
| 34 |
-
<DialogPortal>
|
| 35 |
-
<DialogOverlay />
|
| 36 |
-
<DialogPrimitive.Content
|
| 37 |
-
ref={ref}
|
| 38 |
-
className={cn(
|
| 39 |
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
| 40 |
-
className
|
| 41 |
-
)}
|
| 42 |
-
{...props}
|
| 43 |
-
>
|
| 44 |
-
{children}
|
| 45 |
-
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
| 46 |
-
<X className="h-4 w-4" />
|
| 47 |
-
<span className="sr-only">Close</span>
|
| 48 |
-
</DialogPrimitive.Close>
|
| 49 |
-
</DialogPrimitive.Content>
|
| 50 |
-
</DialogPortal>
|
| 51 |
-
));
|
| 52 |
-
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
| 53 |
-
|
| 54 |
-
const DialogHeader = ({
|
| 55 |
-
className,
|
| 56 |
-
...props
|
| 57 |
-
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 58 |
-
<div
|
| 59 |
-
className={cn(
|
| 60 |
-
"flex flex-col space-y-1.5 text-center sm:text-left",
|
| 61 |
-
className
|
| 62 |
-
)}
|
| 63 |
-
{...props}
|
| 64 |
-
/>
|
| 65 |
-
);
|
| 66 |
-
DialogHeader.displayName = "DialogHeader";
|
| 67 |
-
|
| 68 |
-
const DialogFooter = ({
|
| 69 |
-
className,
|
| 70 |
-
...props
|
| 71 |
-
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 72 |
-
<div
|
| 73 |
-
className={cn(
|
| 74 |
-
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
| 75 |
-
className
|
| 76 |
-
)}
|
| 77 |
-
{...props}
|
| 78 |
-
/>
|
| 79 |
-
);
|
| 80 |
-
DialogFooter.displayName = "DialogFooter";
|
| 81 |
-
|
| 82 |
-
const DialogTitle = React.forwardRef<
|
| 83 |
-
React.ElementRef<typeof DialogPrimitive.Title>,
|
| 84 |
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
| 85 |
-
>(({ className, ...props }, ref) => (
|
| 86 |
-
<DialogPrimitive.Title
|
| 87 |
-
ref={ref}
|
| 88 |
-
className={cn(
|
| 89 |
-
"text-lg font-semibold leading-none tracking-tight",
|
| 90 |
-
className
|
| 91 |
-
)}
|
| 92 |
-
{...props}
|
| 93 |
-
/>
|
| 94 |
-
));
|
| 95 |
-
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
| 96 |
-
|
| 97 |
-
const DialogDescription = React.forwardRef<
|
| 98 |
-
React.ElementRef<typeof DialogPrimitive.Description>,
|
| 99 |
-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
| 100 |
-
>(({ className, ...props }, ref) => (
|
| 101 |
-
<DialogPrimitive.Description
|
| 102 |
-
ref={ref}
|
| 103 |
-
className={cn("text-sm text-muted-foreground", className)}
|
| 104 |
-
{...props}
|
| 105 |
-
/>
|
| 106 |
-
));
|
| 107 |
-
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
| 108 |
-
|
| 109 |
-
export {
|
| 110 |
-
Dialog,
|
| 111 |
-
DialogPortal,
|
| 112 |
-
DialogOverlay,
|
| 113 |
-
DialogClose,
|
| 114 |
-
DialogContent,
|
| 115 |
-
DialogDescription,
|
| 116 |
-
DialogFooter,
|
| 117 |
-
DialogHeader,
|
| 118 |
-
DialogTitle,
|
| 119 |
-
DialogTrigger,
|
| 120 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/components/ui/progress.tsx
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
| 3 |
-
|
| 4 |
-
import { cn } from "../../lib/utils";
|
| 5 |
-
|
| 6 |
-
const Progress = React.forwardRef<
|
| 7 |
-
React.ElementRef<typeof ProgressPrimitive.Root>,
|
| 8 |
-
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
| 9 |
-
>(({ className, value, ...props }, ref) => (
|
| 10 |
-
<ProgressPrimitive.Root
|
| 11 |
-
ref={ref}
|
| 12 |
-
className={cn(
|
| 13 |
-
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
| 14 |
-
className
|
| 15 |
-
)}
|
| 16 |
-
{...props}
|
| 17 |
-
>
|
| 18 |
-
<ProgressPrimitive.Indicator
|
| 19 |
-
className="h-full w-full flex-1 bg-primary transition-all"
|
| 20 |
-
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
| 21 |
-
/>
|
| 22 |
-
</ProgressPrimitive.Root>
|
| 23 |
-
));
|
| 24 |
-
Progress.displayName = ProgressPrimitive.Root.displayName;
|
| 25 |
-
|
| 26 |
-
export { Progress };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/index.css
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
@tailwind base;
|
| 2 |
-
@tailwind components;
|
| 3 |
-
@tailwind utilities;
|
| 4 |
-
|
| 5 |
-
@layer base {
|
| 6 |
-
* {
|
| 7 |
-
@apply border-border;
|
| 8 |
-
}
|
| 9 |
-
body {
|
| 10 |
-
@apply bg-background text-foreground;
|
| 11 |
-
}
|
| 12 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/lib/unified-storage.ts
DELETED
|
@@ -1,325 +0,0 @@
|
|
| 1 |
-
// Unified storage system for robot data
|
| 2 |
-
// Consolidates robot config, calibration data, and metadata under one key per device
|
| 3 |
-
|
| 4 |
-
export interface UnifiedRobotData {
|
| 5 |
-
device_info: {
|
| 6 |
-
serialNumber: string;
|
| 7 |
-
robotType: "so100_follower" | "so100_leader";
|
| 8 |
-
robotId: string;
|
| 9 |
-
usbMetadata?: any;
|
| 10 |
-
lastUpdated: string;
|
| 11 |
-
};
|
| 12 |
-
calibration?: {
|
| 13 |
-
// Motor calibration data (from lerobot_calibration_* keys)
|
| 14 |
-
shoulder_pan?: {
|
| 15 |
-
id: number;
|
| 16 |
-
drive_mode: number;
|
| 17 |
-
homing_offset: number;
|
| 18 |
-
range_min: number;
|
| 19 |
-
range_max: number;
|
| 20 |
-
};
|
| 21 |
-
shoulder_lift?: {
|
| 22 |
-
id: number;
|
| 23 |
-
drive_mode: number;
|
| 24 |
-
homing_offset: number;
|
| 25 |
-
range_min: number;
|
| 26 |
-
range_max: number;
|
| 27 |
-
};
|
| 28 |
-
elbow_flex?: {
|
| 29 |
-
id: number;
|
| 30 |
-
drive_mode: number;
|
| 31 |
-
homing_offset: number;
|
| 32 |
-
range_min: number;
|
| 33 |
-
range_max: number;
|
| 34 |
-
};
|
| 35 |
-
wrist_flex?: {
|
| 36 |
-
id: number;
|
| 37 |
-
drive_mode: number;
|
| 38 |
-
homing_offset: number;
|
| 39 |
-
range_min: number;
|
| 40 |
-
range_max: number;
|
| 41 |
-
};
|
| 42 |
-
wrist_roll?: {
|
| 43 |
-
id: number;
|
| 44 |
-
drive_mode: number;
|
| 45 |
-
homing_offset: number;
|
| 46 |
-
range_min: number;
|
| 47 |
-
range_max: number;
|
| 48 |
-
};
|
| 49 |
-
gripper?: {
|
| 50 |
-
id: number;
|
| 51 |
-
drive_mode: number;
|
| 52 |
-
homing_offset: number;
|
| 53 |
-
range_min: number;
|
| 54 |
-
range_max: number;
|
| 55 |
-
};
|
| 56 |
-
|
| 57 |
-
// Calibration metadata (from lerobot-calibration-* keys)
|
| 58 |
-
metadata: {
|
| 59 |
-
timestamp: string;
|
| 60 |
-
readCount: number;
|
| 61 |
-
platform: string;
|
| 62 |
-
api: string;
|
| 63 |
-
device_type: string;
|
| 64 |
-
device_id: string;
|
| 65 |
-
calibrated_at: string;
|
| 66 |
-
};
|
| 67 |
-
};
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
/**
|
| 71 |
-
* Get unified storage key for a robot by serial number
|
| 72 |
-
*/
|
| 73 |
-
export function getUnifiedKey(serialNumber: string): string {
|
| 74 |
-
return `lerobotjs-${serialNumber}`;
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
/**
|
| 78 |
-
* Migrate data from old storage keys to unified format
|
| 79 |
-
* Safely combines data from three sources:
|
| 80 |
-
* 1. lerobot-robot-{serialNumber} - robot config
|
| 81 |
-
* 2. lerobot-calibration-{serialNumber} - calibration metadata
|
| 82 |
-
* 3. lerobot_calibration_{robotType}_{robotId} - actual calibration data
|
| 83 |
-
*/
|
| 84 |
-
export function migrateToUnifiedStorage(
|
| 85 |
-
serialNumber: string
|
| 86 |
-
): UnifiedRobotData | null {
|
| 87 |
-
try {
|
| 88 |
-
const unifiedKey = getUnifiedKey(serialNumber);
|
| 89 |
-
|
| 90 |
-
// Check if already migrated
|
| 91 |
-
const existing = localStorage.getItem(unifiedKey);
|
| 92 |
-
if (existing) {
|
| 93 |
-
console.log(`✅ Data already unified for ${serialNumber}`);
|
| 94 |
-
return JSON.parse(existing);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
console.log(`🔄 Migrating data for serial number: ${serialNumber}`);
|
| 98 |
-
|
| 99 |
-
// 1. Get robot configuration
|
| 100 |
-
const robotConfigKey = `lerobot-robot-${serialNumber}`;
|
| 101 |
-
const robotConfigRaw = localStorage.getItem(robotConfigKey);
|
| 102 |
-
|
| 103 |
-
if (!robotConfigRaw) {
|
| 104 |
-
return null;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
const robotConfig = JSON.parse(robotConfigRaw);
|
| 108 |
-
console.log(`📋 Found robot config:`, robotConfig);
|
| 109 |
-
|
| 110 |
-
// 2. Get calibration metadata
|
| 111 |
-
const calibrationMetaKey = `lerobot-calibration-${serialNumber}`;
|
| 112 |
-
const calibrationMetaRaw = localStorage.getItem(calibrationMetaKey);
|
| 113 |
-
const calibrationMeta = calibrationMetaRaw
|
| 114 |
-
? JSON.parse(calibrationMetaRaw)
|
| 115 |
-
: null;
|
| 116 |
-
console.log(`📊 Found calibration metadata:`, calibrationMeta);
|
| 117 |
-
|
| 118 |
-
// 3. Get actual calibration data (using robotType and robotId from config)
|
| 119 |
-
const calibrationDataKey = `lerobot_calibration_${robotConfig.robotType}_${robotConfig.robotId}`;
|
| 120 |
-
const calibrationDataRaw = localStorage.getItem(calibrationDataKey);
|
| 121 |
-
const calibrationData = calibrationDataRaw
|
| 122 |
-
? JSON.parse(calibrationDataRaw)
|
| 123 |
-
: null;
|
| 124 |
-
console.log(`🔧 Found calibration data:`, calibrationData);
|
| 125 |
-
|
| 126 |
-
// 4. Build unified structure
|
| 127 |
-
const unifiedData: UnifiedRobotData = {
|
| 128 |
-
device_info: {
|
| 129 |
-
serialNumber: robotConfig.serialNumber || serialNumber,
|
| 130 |
-
robotType: robotConfig.robotType,
|
| 131 |
-
robotId: robotConfig.robotId,
|
| 132 |
-
lastUpdated: robotConfig.lastUpdated || new Date().toISOString(),
|
| 133 |
-
},
|
| 134 |
-
};
|
| 135 |
-
|
| 136 |
-
// Add calibration if available
|
| 137 |
-
if (calibrationData && calibrationMeta) {
|
| 138 |
-
const motors: any = {};
|
| 139 |
-
|
| 140 |
-
// Copy motor data (excluding metadata fields)
|
| 141 |
-
Object.keys(calibrationData).forEach((key) => {
|
| 142 |
-
if (
|
| 143 |
-
![
|
| 144 |
-
"device_type",
|
| 145 |
-
"device_id",
|
| 146 |
-
"calibrated_at",
|
| 147 |
-
"platform",
|
| 148 |
-
"api",
|
| 149 |
-
].includes(key)
|
| 150 |
-
) {
|
| 151 |
-
motors[key] = calibrationData[key];
|
| 152 |
-
}
|
| 153 |
-
});
|
| 154 |
-
|
| 155 |
-
unifiedData.calibration = {
|
| 156 |
-
...motors,
|
| 157 |
-
metadata: {
|
| 158 |
-
timestamp: calibrationMeta.timestamp || calibrationData.calibrated_at,
|
| 159 |
-
readCount: calibrationMeta.readCount || 0,
|
| 160 |
-
platform: calibrationData.platform || "web",
|
| 161 |
-
api: calibrationData.api || "Web Serial API",
|
| 162 |
-
device_type: calibrationData.device_type || robotConfig.robotType,
|
| 163 |
-
device_id: calibrationData.device_id || robotConfig.robotId,
|
| 164 |
-
calibrated_at:
|
| 165 |
-
calibrationData.calibrated_at || calibrationMeta.timestamp,
|
| 166 |
-
},
|
| 167 |
-
};
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
// 5. Save unified data
|
| 171 |
-
localStorage.setItem(unifiedKey, JSON.stringify(unifiedData));
|
| 172 |
-
console.log(`✅ Successfully unified data for ${serialNumber}`);
|
| 173 |
-
console.log(`📦 Unified data:`, unifiedData);
|
| 174 |
-
|
| 175 |
-
// 6. Clean up old keys (optional - keep for now for safety)
|
| 176 |
-
// localStorage.removeItem(robotConfigKey);
|
| 177 |
-
// localStorage.removeItem(calibrationMetaKey);
|
| 178 |
-
// localStorage.removeItem(calibrationDataKey);
|
| 179 |
-
|
| 180 |
-
return unifiedData;
|
| 181 |
-
} catch (error) {
|
| 182 |
-
console.error(`❌ Failed to migrate data for ${serialNumber}:`, error);
|
| 183 |
-
return null;
|
| 184 |
-
}
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
/**
|
| 188 |
-
* Get unified robot data
|
| 189 |
-
*/
|
| 190 |
-
export function getUnifiedRobotData(
|
| 191 |
-
serialNumber: string
|
| 192 |
-
): UnifiedRobotData | null {
|
| 193 |
-
const unifiedKey = getUnifiedKey(serialNumber);
|
| 194 |
-
|
| 195 |
-
// Try to get existing unified data
|
| 196 |
-
const existing = localStorage.getItem(unifiedKey);
|
| 197 |
-
if (existing) {
|
| 198 |
-
try {
|
| 199 |
-
return JSON.parse(existing);
|
| 200 |
-
} catch (error) {
|
| 201 |
-
console.warn(`Failed to parse unified data for ${serialNumber}:`, error);
|
| 202 |
-
}
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
return null;
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
/**
|
| 209 |
-
* Save robot configuration to unified storage
|
| 210 |
-
*/
|
| 211 |
-
export function saveRobotConfig(
|
| 212 |
-
serialNumber: string,
|
| 213 |
-
robotType: "so100_follower" | "so100_leader",
|
| 214 |
-
robotId: string,
|
| 215 |
-
usbMetadata?: any
|
| 216 |
-
): void {
|
| 217 |
-
const unifiedKey = getUnifiedKey(serialNumber);
|
| 218 |
-
const existing =
|
| 219 |
-
getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
|
| 220 |
-
|
| 221 |
-
existing.device_info = {
|
| 222 |
-
serialNumber,
|
| 223 |
-
robotType,
|
| 224 |
-
robotId,
|
| 225 |
-
usbMetadata,
|
| 226 |
-
lastUpdated: new Date().toISOString(),
|
| 227 |
-
};
|
| 228 |
-
|
| 229 |
-
localStorage.setItem(unifiedKey, JSON.stringify(existing));
|
| 230 |
-
console.log(`💾 Saved robot config for ${serialNumber}`);
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
/**
|
| 234 |
-
* Save calibration data to unified storage
|
| 235 |
-
*/
|
| 236 |
-
export function saveCalibrationData(
|
| 237 |
-
serialNumber: string,
|
| 238 |
-
calibrationData: any,
|
| 239 |
-
metadata: { timestamp: string; readCount: number }
|
| 240 |
-
): void {
|
| 241 |
-
const unifiedKey = getUnifiedKey(serialNumber);
|
| 242 |
-
const existing =
|
| 243 |
-
getUnifiedRobotData(serialNumber) || ({} as UnifiedRobotData);
|
| 244 |
-
|
| 245 |
-
// Ensure device_info exists
|
| 246 |
-
if (!existing.device_info) {
|
| 247 |
-
console.warn(
|
| 248 |
-
`No device info found for ${serialNumber}, cannot save calibration`
|
| 249 |
-
);
|
| 250 |
-
return;
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
// Extract motor data (exclude metadata fields)
|
| 254 |
-
const motors: any = {};
|
| 255 |
-
Object.keys(calibrationData).forEach((key) => {
|
| 256 |
-
if (
|
| 257 |
-
![
|
| 258 |
-
"device_type",
|
| 259 |
-
"device_id",
|
| 260 |
-
"calibrated_at",
|
| 261 |
-
"platform",
|
| 262 |
-
"api",
|
| 263 |
-
].includes(key)
|
| 264 |
-
) {
|
| 265 |
-
motors[key] = calibrationData[key];
|
| 266 |
-
}
|
| 267 |
-
});
|
| 268 |
-
|
| 269 |
-
existing.calibration = {
|
| 270 |
-
...motors,
|
| 271 |
-
metadata: {
|
| 272 |
-
timestamp: metadata.timestamp,
|
| 273 |
-
readCount: metadata.readCount,
|
| 274 |
-
platform: calibrationData.platform || "web",
|
| 275 |
-
api: calibrationData.api || "Web Serial API",
|
| 276 |
-
device_type:
|
| 277 |
-
calibrationData.device_type || existing.device_info.robotType,
|
| 278 |
-
device_id: calibrationData.device_id || existing.device_info.robotId,
|
| 279 |
-
calibrated_at: calibrationData.calibrated_at || metadata.timestamp,
|
| 280 |
-
},
|
| 281 |
-
};
|
| 282 |
-
|
| 283 |
-
localStorage.setItem(unifiedKey, JSON.stringify(existing));
|
| 284 |
-
console.log(`🔧 Saved calibration data for ${serialNumber}`);
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
/**
|
| 288 |
-
* Check if robot is calibrated
|
| 289 |
-
*/
|
| 290 |
-
export function isRobotCalibrated(serialNumber: string): boolean {
|
| 291 |
-
const data = getUnifiedRobotData(serialNumber);
|
| 292 |
-
return !!data?.calibration?.metadata?.timestamp;
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
/**
|
| 296 |
-
* Get calibration status for dashboard
|
| 297 |
-
*/
|
| 298 |
-
export function getCalibrationStatus(
|
| 299 |
-
serialNumber: string
|
| 300 |
-
): { timestamp: string; readCount: number } | null {
|
| 301 |
-
const data = getUnifiedRobotData(serialNumber);
|
| 302 |
-
if (data?.calibration?.metadata) {
|
| 303 |
-
return {
|
| 304 |
-
timestamp: data.calibration.metadata.timestamp,
|
| 305 |
-
readCount: data.calibration.metadata.readCount,
|
| 306 |
-
};
|
| 307 |
-
}
|
| 308 |
-
return null;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
/**
|
| 312 |
-
* Get robot configuration
|
| 313 |
-
*/
|
| 314 |
-
export function getRobotConfig(
|
| 315 |
-
serialNumber: string
|
| 316 |
-
): { robotType: string; robotId: string } | null {
|
| 317 |
-
const data = getUnifiedRobotData(serialNumber);
|
| 318 |
-
if (data?.device_info) {
|
| 319 |
-
return {
|
| 320 |
-
robotType: data.device_info.robotType,
|
| 321 |
-
robotId: data.device_info.robotId,
|
| 322 |
-
};
|
| 323 |
-
}
|
| 324 |
-
return null;
|
| 325 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/lib/utils.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
import { type ClassValue, clsx } from "clsx";
|
| 2 |
-
import { twMerge } from "tailwind-merge";
|
| 3 |
-
|
| 4 |
-
export function cn(...inputs: ClassValue[]) {
|
| 5 |
-
return twMerge(clsx(inputs));
|
| 6 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/main.tsx
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
import ReactDOM from "react-dom/client";
|
| 2 |
-
import { App } from "./App";
|
| 3 |
-
import "./index.css";
|
| 4 |
-
|
| 5 |
-
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/pages/Home.tsx
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
import { useState } from "react";
|
| 2 |
-
import { Button } from "../components/ui/button";
|
| 3 |
-
import { Alert, AlertDescription } from "../components/ui/alert";
|
| 4 |
-
import { PortManager } from "../components/PortManager";
|
| 5 |
-
import { CalibrationPanel } from "../components/CalibrationPanel";
|
| 6 |
-
import { TeleoperationPanel } from "../components/TeleoperationPanel";
|
| 7 |
-
import { isWebSerialSupported } from "@lerobot/web";
|
| 8 |
-
import type { RobotConnection } from "@lerobot/web";
|
| 9 |
-
|
| 10 |
-
interface HomeProps {
|
| 11 |
-
connectedRobots: RobotConnection[];
|
| 12 |
-
onConnectedRobotsChange: (robots: RobotConnection[]) => void;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
export function Home({ connectedRobots, onConnectedRobotsChange }: HomeProps) {
|
| 16 |
-
const [calibratingRobot, setCalibratingRobot] =
|
| 17 |
-
useState<RobotConnection | null>(null);
|
| 18 |
-
const [teleoperatingRobot, setTeleoperatingRobot] =
|
| 19 |
-
useState<RobotConnection | null>(null);
|
| 20 |
-
const isSupported = isWebSerialSupported();
|
| 21 |
-
|
| 22 |
-
const handleCalibrate = (port: SerialPort) => {
|
| 23 |
-
// Find the robot from connectedRobots
|
| 24 |
-
const robot = connectedRobots.find((r) => r.port === port);
|
| 25 |
-
if (robot) {
|
| 26 |
-
setCalibratingRobot(robot);
|
| 27 |
-
}
|
| 28 |
-
};
|
| 29 |
-
|
| 30 |
-
const handleTeleoperate = (robot: RobotConnection) => {
|
| 31 |
-
setTeleoperatingRobot(robot);
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
const handleFinishCalibration = () => {
|
| 35 |
-
setCalibratingRobot(null);
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
const handleFinishTeleoperation = () => {
|
| 39 |
-
setTeleoperatingRobot(null);
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
return (
|
| 43 |
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
| 44 |
-
<div className="container mx-auto px-6 py-12">
|
| 45 |
-
{/* Header */}
|
| 46 |
-
<div className="text-center mb-12">
|
| 47 |
-
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
| 48 |
-
🤖 LeRobot.js
|
| 49 |
-
</h1>
|
| 50 |
-
<p className="text-xl text-gray-600 mb-8">
|
| 51 |
-
Robotics for the web and node
|
| 52 |
-
</p>
|
| 53 |
-
|
| 54 |
-
{!isSupported && (
|
| 55 |
-
<Alert variant="destructive" className="max-w-2xl mx-auto mb-8">
|
| 56 |
-
<AlertDescription>
|
| 57 |
-
Web Serial API is not supported in this browser. Please use
|
| 58 |
-
Chrome, Edge, or another Chromium-based browser to use this
|
| 59 |
-
demo.
|
| 60 |
-
</AlertDescription>
|
| 61 |
-
</Alert>
|
| 62 |
-
)}
|
| 63 |
-
</div>
|
| 64 |
-
|
| 65 |
-
{/* Main Content */}
|
| 66 |
-
{calibratingRobot ? (
|
| 67 |
-
<div className="max-w-6xl mx-auto">
|
| 68 |
-
<div className="mb-4">
|
| 69 |
-
<Button
|
| 70 |
-
variant="outline"
|
| 71 |
-
onClick={() => setCalibratingRobot(null)}
|
| 72 |
-
>
|
| 73 |
-
← Back to Dashboard
|
| 74 |
-
</Button>
|
| 75 |
-
</div>
|
| 76 |
-
<CalibrationPanel
|
| 77 |
-
robot={calibratingRobot}
|
| 78 |
-
onFinish={handleFinishCalibration}
|
| 79 |
-
/>
|
| 80 |
-
</div>
|
| 81 |
-
) : teleoperatingRobot ? (
|
| 82 |
-
<TeleoperationPanel
|
| 83 |
-
robot={teleoperatingRobot}
|
| 84 |
-
onClose={handleFinishTeleoperation}
|
| 85 |
-
/>
|
| 86 |
-
) : (
|
| 87 |
-
<div className="max-w-6xl mx-auto">
|
| 88 |
-
<PortManager
|
| 89 |
-
onCalibrate={handleCalibrate}
|
| 90 |
-
onTeleoperate={handleTeleoperate}
|
| 91 |
-
connectedRobots={connectedRobots}
|
| 92 |
-
onConnectedRobotsChange={onConnectedRobotsChange}
|
| 93 |
-
/>
|
| 94 |
-
</div>
|
| 95 |
-
)}
|
| 96 |
-
</div>
|
| 97 |
-
</div>
|
| 98 |
-
);
|
| 99 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/demo/types.ts
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
// Demo uses the standard library RobotConnection interface directly
|
| 2 |
-
// No need for separate types - just import RobotConnection where needed
|
|
|
|
|
|
|
|
|
tailwind.config.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
/** @type {import('tailwindcss').Config} */
|
| 2 |
export default {
|
| 3 |
-
content: [
|
|
|
|
|
|
|
|
|
|
| 4 |
theme: {
|
| 5 |
extend: {
|
| 6 |
borderRadius: {
|
|
|
|
| 1 |
/** @type {import('tailwindcss').Config} */
|
| 2 |
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./examples/robot-control-web/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
theme: {
|
| 8 |
extend: {
|
| 9 |
borderRadius: {
|
tsconfig.json
CHANGED
|
@@ -24,5 +24,5 @@
|
|
| 24 |
"noFallthroughCasesInSwitch": true,
|
| 25 |
"noUncheckedSideEffectImports": true
|
| 26 |
},
|
| 27 |
-
"include": ["src"]
|
| 28 |
}
|
|
|
|
| 24 |
"noFallthroughCasesInSwitch": true,
|
| 25 |
"noUncheckedSideEffectImports": true
|
| 26 |
},
|
| 27 |
+
"include": ["src", "examples/robot-control-web"]
|
| 28 |
}
|