Spaces:
Running
Running
feat: make the cyberpunk example work with @lerobot/web
Browse files- examples/cyberpunk-standalone/package.json +1 -0
- examples/cyberpunk-standalone/src/App.tsx +200 -45
- examples/cyberpunk-standalone/src/components/VirtualKey.tsx +53 -10
- examples/cyberpunk-standalone/src/components/calibration-view.tsx +235 -53
- examples/cyberpunk-standalone/src/components/device-dashboard.tsx +77 -33
- examples/cyberpunk-standalone/src/components/edit-robot-dialog.tsx +49 -24
- examples/cyberpunk-standalone/src/components/teleoperation-view.tsx +565 -82
- examples/cyberpunk-standalone/src/global.css +1 -25
- examples/cyberpunk-standalone/src/lib/mock-api.ts +0 -137
- examples/cyberpunk-standalone/src/lib/unified-storage.ts +137 -0
- examples/cyberpunk-standalone/src/types/robot.ts +6 -35
- examples/cyberpunk-standalone/tsconfig.tsbuildinfo +0 -0
examples/cyberpunk-standalone/package.json
CHANGED
@@ -10,6 +10,7 @@
|
|
10 |
"preview": "vite preview"
|
11 |
},
|
12 |
"dependencies": {
|
|
|
13 |
"@hookform/resolvers": "^3.9.1",
|
14 |
"@radix-ui/react-accordion": "1.2.2",
|
15 |
"@radix-ui/react-alert-dialog": "1.1.4",
|
|
|
10 |
"preview": "vite preview"
|
11 |
},
|
12 |
"dependencies": {
|
13 |
+
"@lerobot/web": "workspace:*",
|
14 |
"@hookform/resolvers": "^3.9.1",
|
15 |
"@radix-ui/react-accordion": "1.2.2",
|
16 |
"@radix-ui/react-alert-dialog": "1.1.4",
|
examples/cyberpunk-standalone/src/App.tsx
CHANGED
@@ -10,80 +10,241 @@ import { SetupCards } from "@/components/setup-cards";
|
|
10 |
import { DocsSection } from "@/components/docs-section";
|
11 |
import { RoadmapSection } from "@/components/roadmap-section";
|
12 |
import { HardwareSupportSection } from "@/components/hardware-support-section";
|
13 |
-
import {
|
14 |
-
import {
|
15 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
function App() {
|
18 |
const [view, setView] = useState<
|
19 |
"dashboard" | "calibrating" | "teleoperating"
|
20 |
>("dashboard");
|
21 |
-
const [robots, setRobots] =
|
22 |
-
"connected-robots",
|
23 |
-
[]
|
24 |
-
);
|
25 |
const [selectedRobot, setSelectedRobot] = useState<RobotConnection | null>(
|
26 |
null
|
27 |
);
|
28 |
const [editingRobot, setEditingRobot] = useState<RobotConnection | null>(
|
29 |
null
|
30 |
);
|
31 |
-
const [logs, setLogs] = useState<string[]>([]);
|
32 |
const [isConnecting, setIsConnecting] = useState(false);
|
33 |
const hardwareSectionRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
useEffect(() => {
|
36 |
const loadSavedRobots = async () => {
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
});
|
43 |
-
|
44 |
-
|
45 |
}
|
46 |
-
setIsConnecting(false);
|
47 |
};
|
|
|
48 |
loadSavedRobots();
|
49 |
-
}, []);
|
50 |
|
51 |
const handleFindNewRobots = async () => {
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
}
|
67 |
-
setIsConnecting(false);
|
68 |
};
|
69 |
|
70 |
const handleUpdateRobot = (updatedRobot: RobotConnection) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
setRobots((prev) =>
|
72 |
-
prev.map((r) =>
|
|
|
|
|
73 |
);
|
74 |
setEditingRobot(null);
|
75 |
};
|
76 |
|
77 |
const handleRemoveRobot = (robotId: string) => {
|
|
|
|
|
|
|
|
|
|
|
78 |
setRobots((prev) => prev.filter((r) => r.robotId !== robotId));
|
|
|
|
|
|
|
|
|
|
|
79 |
};
|
80 |
|
81 |
const handleCalibrate = (robot: RobotConnection) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
setSelectedRobot(robot);
|
83 |
setView("calibrating");
|
84 |
};
|
85 |
|
86 |
const handleTeleoperate = (robot: RobotConnection) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
setSelectedRobot(robot);
|
88 |
setView("teleoperating");
|
89 |
};
|
@@ -139,13 +300,6 @@ function App() {
|
|
139 |
};
|
140 |
|
141 |
const PageHeader = () => {
|
142 |
-
let title = "DASHBOARD";
|
143 |
-
if (view === "calibrating" && selectedRobot) {
|
144 |
-
title = `CALIBRATE: ${selectedRobot.name.toUpperCase()}`;
|
145 |
-
} else if (view === "teleoperating" && selectedRobot) {
|
146 |
-
title = `TELEOPERATE: ${selectedRobot.name.toUpperCase()}`;
|
147 |
-
}
|
148 |
-
|
149 |
return (
|
150 |
<div className="flex items-center justify-between mb-12">
|
151 |
<div className="flex items-center gap-4">
|
@@ -157,9 +311,9 @@ function App() {
|
|
157 |
</span>{" "}
|
158 |
<span
|
159 |
className="text-primary text-glitch uppercase"
|
160 |
-
data-text={selectedRobot.
|
161 |
>
|
162 |
-
{selectedRobot.
|
163 |
</span>
|
164 |
</h1>
|
165 |
) : view === "teleoperating" && selectedRobot ? (
|
@@ -169,9 +323,9 @@ function App() {
|
|
169 |
</span>{" "}
|
170 |
<span
|
171 |
className="text-primary text-glitch uppercase"
|
172 |
-
data-text={selectedRobot.
|
173 |
>
|
174 |
-
{selectedRobot.
|
175 |
</span>
|
176 |
</h1>
|
177 |
) : (
|
@@ -202,7 +356,7 @@ function App() {
|
|
202 |
};
|
203 |
|
204 |
return (
|
205 |
-
<div className="flex flex-col min-h-screen font-sans
|
206 |
<Header />
|
207 |
<main className="flex-grow container mx-auto py-12 px-4 md:px-6">
|
208 |
<PageHeader />
|
@@ -215,6 +369,7 @@ function App() {
|
|
215 |
/>
|
216 |
</main>
|
217 |
<Footer />
|
|
|
218 |
</div>
|
219 |
);
|
220 |
}
|
|
|
10 |
import { DocsSection } from "@/components/docs-section";
|
11 |
import { RoadmapSection } from "@/components/roadmap-section";
|
12 |
import { HardwareSupportSection } from "@/components/hardware-support-section";
|
13 |
+
import { useToast } from "@/hooks/use-toast";
|
14 |
+
import { Toaster } from "@/components/ui/toaster";
|
15 |
+
import {
|
16 |
+
findPort,
|
17 |
+
isWebSerialSupported,
|
18 |
+
type RobotConnection,
|
19 |
+
type RobotConfig,
|
20 |
+
} from "@lerobot/web";
|
21 |
+
import {
|
22 |
+
getAllSavedRobots,
|
23 |
+
getUnifiedRobotData,
|
24 |
+
saveDeviceInfo,
|
25 |
+
removeRobotData,
|
26 |
+
type DeviceInfo,
|
27 |
+
} from "@/lib/unified-storage";
|
28 |
|
29 |
function App() {
|
30 |
const [view, setView] = useState<
|
31 |
"dashboard" | "calibrating" | "teleoperating"
|
32 |
>("dashboard");
|
33 |
+
const [robots, setRobots] = useState<RobotConnection[]>([]);
|
|
|
|
|
|
|
34 |
const [selectedRobot, setSelectedRobot] = useState<RobotConnection | null>(
|
35 |
null
|
36 |
);
|
37 |
const [editingRobot, setEditingRobot] = useState<RobotConnection | null>(
|
38 |
null
|
39 |
);
|
|
|
40 |
const [isConnecting, setIsConnecting] = useState(false);
|
41 |
const hardwareSectionRef = useRef<HTMLDivElement>(null);
|
42 |
+
const { toast } = useToast();
|
43 |
+
|
44 |
+
// Check browser support
|
45 |
+
const isSupported = isWebSerialSupported();
|
46 |
+
|
47 |
+
useEffect(() => {
|
48 |
+
if (!isSupported) {
|
49 |
+
toast({
|
50 |
+
title: "Browser Not Supported",
|
51 |
+
description:
|
52 |
+
"WebSerial API is not supported. Please use Chrome, Edge, or another Chromium-based browser.",
|
53 |
+
variant: "destructive",
|
54 |
+
});
|
55 |
+
}
|
56 |
+
}, [isSupported, toast]);
|
57 |
|
58 |
useEffect(() => {
|
59 |
const loadSavedRobots = async () => {
|
60 |
+
if (!isSupported) return;
|
61 |
+
|
62 |
+
try {
|
63 |
+
setIsConnecting(true);
|
64 |
+
|
65 |
+
// Get saved robot configurations
|
66 |
+
const savedRobots = getAllSavedRobots();
|
67 |
+
|
68 |
+
if (savedRobots.length > 0) {
|
69 |
+
const robotConfigs: RobotConfig[] = savedRobots.map((device) => ({
|
70 |
+
robotType: device.robotType as "so100_follower" | "so100_leader",
|
71 |
+
robotId: device.robotId,
|
72 |
+
serialNumber: device.serialNumber,
|
73 |
+
}));
|
74 |
+
|
75 |
+
// Auto-connect to saved robots
|
76 |
+
const findPortProcess = await findPort({
|
77 |
+
robotConfigs,
|
78 |
+
onMessage: (msg: string) => {
|
79 |
+
console.log("Connection message:", msg);
|
80 |
+
},
|
81 |
+
});
|
82 |
+
|
83 |
+
const reconnectedRobots = await findPortProcess.result;
|
84 |
+
|
85 |
+
// Merge saved device info (names, etc.) with fresh connection data
|
86 |
+
const robotsWithSavedInfo = reconnectedRobots.map((robot) => {
|
87 |
+
const savedData = getUnifiedRobotData(robot.serialNumber || "");
|
88 |
+
if (savedData?.device_info) {
|
89 |
+
return {
|
90 |
+
...robot,
|
91 |
+
robotId: savedData.device_info.robotId,
|
92 |
+
name: savedData.device_info.robotId, // Use the saved custom name
|
93 |
+
robotType: savedData.device_info.robotType as
|
94 |
+
| "so100_follower"
|
95 |
+
| "so100_leader",
|
96 |
+
};
|
97 |
+
}
|
98 |
+
return robot;
|
99 |
+
});
|
100 |
+
|
101 |
+
setRobots(robotsWithSavedInfo);
|
102 |
+
}
|
103 |
+
} catch (error) {
|
104 |
+
console.error("Failed to load saved robots:", error);
|
105 |
+
toast({
|
106 |
+
title: "Connection Error",
|
107 |
+
description: "Failed to reconnect to saved robots",
|
108 |
+
variant: "destructive",
|
109 |
});
|
110 |
+
} finally {
|
111 |
+
setIsConnecting(false);
|
112 |
}
|
|
|
113 |
};
|
114 |
+
|
115 |
loadSavedRobots();
|
116 |
+
}, [isSupported, toast]);
|
117 |
|
118 |
const handleFindNewRobots = async () => {
|
119 |
+
if (!isSupported) {
|
120 |
+
toast({
|
121 |
+
title: "Browser Not Supported",
|
122 |
+
description: "WebSerial API is required for robot connection",
|
123 |
+
variant: "destructive",
|
124 |
+
});
|
125 |
+
return;
|
126 |
+
}
|
127 |
+
|
128 |
+
try {
|
129 |
+
setIsConnecting(true);
|
130 |
+
|
131 |
+
// Interactive mode - show browser dialog
|
132 |
+
const findPortProcess = await findPort({
|
133 |
+
onMessage: (msg: string) => {
|
134 |
+
console.log("Find port message:", msg);
|
135 |
+
},
|
136 |
+
});
|
137 |
+
|
138 |
+
const newRobots = await findPortProcess.result;
|
139 |
+
|
140 |
+
if (newRobots.length > 0) {
|
141 |
+
setRobots((prev: RobotConnection[]) => {
|
142 |
+
const existingSerialNumbers = new Set(
|
143 |
+
prev.map((r: RobotConnection) => r.serialNumber)
|
144 |
+
);
|
145 |
+
const uniqueNewRobots = newRobots.filter(
|
146 |
+
(r: RobotConnection) => !existingSerialNumbers.has(r.serialNumber)
|
147 |
+
);
|
148 |
+
|
149 |
+
// Auto-edit first new robot for configuration
|
150 |
+
if (uniqueNewRobots.length > 0) {
|
151 |
+
setEditingRobot(uniqueNewRobots[0]);
|
152 |
+
}
|
153 |
+
|
154 |
+
return [...prev, ...uniqueNewRobots];
|
155 |
+
});
|
156 |
+
|
157 |
+
toast({
|
158 |
+
title: "Robots Found",
|
159 |
+
description: `Found ${newRobots.length} robot(s)`,
|
160 |
+
});
|
161 |
+
} else {
|
162 |
+
toast({
|
163 |
+
title: "No Robots Found",
|
164 |
+
description: "No compatible devices detected",
|
165 |
+
});
|
166 |
+
}
|
167 |
+
} catch (error) {
|
168 |
+
console.error("Failed to find robots:", error);
|
169 |
+
toast({
|
170 |
+
title: "Connection Error",
|
171 |
+
description: "Failed to find robots. Please try again.",
|
172 |
+
variant: "destructive",
|
173 |
+
});
|
174 |
+
} finally {
|
175 |
+
setIsConnecting(false);
|
176 |
}
|
|
|
177 |
};
|
178 |
|
179 |
const handleUpdateRobot = (updatedRobot: RobotConnection) => {
|
180 |
+
// Save device info to unified storage
|
181 |
+
if (updatedRobot.serialNumber && updatedRobot.robotId) {
|
182 |
+
const deviceInfo: DeviceInfo = {
|
183 |
+
serialNumber: updatedRobot.serialNumber,
|
184 |
+
robotType: updatedRobot.robotType || "so100_follower",
|
185 |
+
robotId: updatedRobot.robotId,
|
186 |
+
usbMetadata: updatedRobot.usbMetadata
|
187 |
+
? {
|
188 |
+
vendorId: parseInt(updatedRobot.usbMetadata.vendorId || "0", 16),
|
189 |
+
productId: parseInt(
|
190 |
+
updatedRobot.usbMetadata.productId || "0",
|
191 |
+
16
|
192 |
+
),
|
193 |
+
serialNumber: updatedRobot.usbMetadata.serialNumber,
|
194 |
+
manufacturer: updatedRobot.usbMetadata.manufacturerName,
|
195 |
+
product: updatedRobot.usbMetadata.productName,
|
196 |
+
}
|
197 |
+
: undefined,
|
198 |
+
};
|
199 |
+
saveDeviceInfo(updatedRobot.serialNumber, deviceInfo);
|
200 |
+
}
|
201 |
+
|
202 |
setRobots((prev) =>
|
203 |
+
prev.map((r) =>
|
204 |
+
r.serialNumber === updatedRobot.serialNumber ? updatedRobot : r
|
205 |
+
)
|
206 |
);
|
207 |
setEditingRobot(null);
|
208 |
};
|
209 |
|
210 |
const handleRemoveRobot = (robotId: string) => {
|
211 |
+
const robot = robots.find((r) => r.robotId === robotId);
|
212 |
+
if (robot?.serialNumber) {
|
213 |
+
removeRobotData(robot.serialNumber);
|
214 |
+
}
|
215 |
+
|
216 |
setRobots((prev) => prev.filter((r) => r.robotId !== robotId));
|
217 |
+
|
218 |
+
toast({
|
219 |
+
title: "Robot Removed",
|
220 |
+
description: `${robotId} has been removed from the registry`,
|
221 |
+
});
|
222 |
};
|
223 |
|
224 |
const handleCalibrate = (robot: RobotConnection) => {
|
225 |
+
if (!robot.isConnected) {
|
226 |
+
toast({
|
227 |
+
title: "Robot Not Connected",
|
228 |
+
description: "Please connect the robot before calibrating",
|
229 |
+
variant: "destructive",
|
230 |
+
});
|
231 |
+
return;
|
232 |
+
}
|
233 |
+
|
234 |
setSelectedRobot(robot);
|
235 |
setView("calibrating");
|
236 |
};
|
237 |
|
238 |
const handleTeleoperate = (robot: RobotConnection) => {
|
239 |
+
if (!robot.isConnected) {
|
240 |
+
toast({
|
241 |
+
title: "Robot Not Connected",
|
242 |
+
description: "Please connect the robot before teleoperating",
|
243 |
+
variant: "destructive",
|
244 |
+
});
|
245 |
+
return;
|
246 |
+
}
|
247 |
+
|
248 |
setSelectedRobot(robot);
|
249 |
setView("teleoperating");
|
250 |
};
|
|
|
300 |
};
|
301 |
|
302 |
const PageHeader = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
303 |
return (
|
304 |
<div className="flex items-center justify-between mb-12">
|
305 |
<div className="flex items-center gap-4">
|
|
|
311 |
</span>{" "}
|
312 |
<span
|
313 |
className="text-primary text-glitch uppercase"
|
314 |
+
data-text={selectedRobot.robotId}
|
315 |
>
|
316 |
+
{selectedRobot.robotId?.toUpperCase()}
|
317 |
</span>
|
318 |
</h1>
|
319 |
) : view === "teleoperating" && selectedRobot ? (
|
|
|
323 |
</span>{" "}
|
324 |
<span
|
325 |
className="text-primary text-glitch uppercase"
|
326 |
+
data-text={selectedRobot.robotId}
|
327 |
>
|
328 |
+
{selectedRobot.robotId?.toUpperCase()}
|
329 |
</span>
|
330 |
</h1>
|
331 |
) : (
|
|
|
356 |
};
|
357 |
|
358 |
return (
|
359 |
+
<div className="flex flex-col min-h-screen font-sans">
|
360 |
<Header />
|
361 |
<main className="flex-grow container mx-auto py-12 px-4 md:px-6">
|
362 |
<PageHeader />
|
|
|
369 |
/>
|
370 |
</main>
|
371 |
<Footer />
|
372 |
+
<Toaster />
|
373 |
</div>
|
374 |
);
|
375 |
}
|
examples/cyberpunk-standalone/src/components/VirtualKey.tsx
CHANGED
@@ -1,27 +1,70 @@
|
|
1 |
-
import { cn } from "@/lib/utils"
|
2 |
|
3 |
interface VirtualKeyProps {
|
4 |
-
label: string
|
5 |
-
subLabel?: string
|
6 |
-
isPressed?: boolean
|
|
|
|
|
|
|
7 |
}
|
8 |
|
9 |
-
const VirtualKey = ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
return (
|
11 |
<div className="flex flex-col items-center">
|
12 |
<div
|
13 |
className={cn(
|
14 |
"w-12 h-12 border rounded-md flex items-center justify-center font-bold transition-all duration-100",
|
|
|
|
|
|
|
|
|
|
|
15 |
isPressed
|
16 |
? "bg-primary text-primary-foreground scale-110 border-primary"
|
17 |
-
: "bg-black/30 text-muted-foreground border-white/10"
|
18 |
)}
|
|
|
|
|
|
|
19 |
>
|
20 |
{label}
|
21 |
</div>
|
22 |
-
{subLabel &&
|
|
|
|
|
|
|
|
|
23 |
</div>
|
24 |
-
)
|
25 |
-
}
|
26 |
|
27 |
-
export default VirtualKey
|
|
|
1 |
+
import { cn } from "@/lib/utils";
|
2 |
|
3 |
interface VirtualKeyProps {
|
4 |
+
label: string;
|
5 |
+
subLabel?: string;
|
6 |
+
isPressed?: boolean;
|
7 |
+
onMouseDown?: () => void;
|
8 |
+
onMouseUp?: () => void;
|
9 |
+
disabled?: boolean;
|
10 |
}
|
11 |
|
12 |
+
const VirtualKey = ({
|
13 |
+
label,
|
14 |
+
subLabel,
|
15 |
+
isPressed,
|
16 |
+
onMouseDown,
|
17 |
+
onMouseUp,
|
18 |
+
disabled,
|
19 |
+
}: VirtualKeyProps) => {
|
20 |
+
const handleMouseDown = (e: React.MouseEvent) => {
|
21 |
+
e.preventDefault();
|
22 |
+
if (!disabled && onMouseDown) {
|
23 |
+
onMouseDown();
|
24 |
+
}
|
25 |
+
};
|
26 |
+
|
27 |
+
const handleMouseUp = (e: React.MouseEvent) => {
|
28 |
+
e.preventDefault();
|
29 |
+
if (!disabled && onMouseUp) {
|
30 |
+
onMouseUp();
|
31 |
+
}
|
32 |
+
};
|
33 |
+
|
34 |
+
const handleMouseLeave = (e: React.MouseEvent) => {
|
35 |
+
e.preventDefault();
|
36 |
+
if (!disabled && onMouseUp) {
|
37 |
+
onMouseUp();
|
38 |
+
}
|
39 |
+
};
|
40 |
+
|
41 |
return (
|
42 |
<div className="flex flex-col items-center">
|
43 |
<div
|
44 |
className={cn(
|
45 |
"w-12 h-12 border rounded-md flex items-center justify-center font-bold transition-all duration-100",
|
46 |
+
"select-none user-select-none",
|
47 |
+
disabled && "opacity-50 cursor-not-allowed",
|
48 |
+
!disabled &&
|
49 |
+
(onMouseDown || onMouseUp) &&
|
50 |
+
"cursor-pointer hover:bg-white/5",
|
51 |
isPressed
|
52 |
? "bg-primary text-primary-foreground scale-110 border-primary"
|
53 |
+
: "bg-black/30 text-muted-foreground border-white/10"
|
54 |
)}
|
55 |
+
onMouseDown={handleMouseDown}
|
56 |
+
onMouseUp={handleMouseUp}
|
57 |
+
onMouseLeave={handleMouseLeave}
|
58 |
>
|
59 |
{label}
|
60 |
</div>
|
61 |
+
{subLabel && (
|
62 |
+
<span className="text-xs text-muted-foreground mt-1 font-mono">
|
63 |
+
{subLabel}
|
64 |
+
</span>
|
65 |
+
)}
|
66 |
</div>
|
67 |
+
);
|
68 |
+
};
|
69 |
|
70 |
+
export default VirtualKey;
|
examples/cyberpunk-standalone/src/components/calibration-view.tsx
CHANGED
@@ -1,67 +1,227 @@
|
|
1 |
-
"use client"
|
2 |
-
import { useState, useMemo } from "react"
|
3 |
-
import { Download } from "lucide-react"
|
4 |
-
import { Button } from "@/components/ui/button"
|
5 |
-
import { Card } from "@/components/ui/card"
|
6 |
-
import {
|
7 |
-
import {
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
interface CalibrationViewProps {
|
12 |
-
robot: RobotConnection
|
13 |
}
|
14 |
|
15 |
export function CalibrationView({ robot }: CalibrationViewProps) {
|
16 |
-
const [status, setStatus] = useState("Ready to calibrate.")
|
17 |
-
const [liveData, setLiveData] = useState<LiveCalibrationData | null>(null)
|
18 |
-
const [isCalibrating, setIsCalibrating] = useState(false)
|
19 |
-
const [
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
const handleStart = async () => {
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
37 |
const handleFinish = async () => {
|
38 |
if (calibrationProcess) {
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}
|
45 |
-
}
|
46 |
|
47 |
const downloadJson = () => {
|
48 |
-
if (!calibrationResults) return
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
const motorData = useMemo(
|
59 |
() =>
|
60 |
liveData
|
61 |
? Object.entries(liveData)
|
62 |
-
:
|
63 |
-
|
64 |
-
|
|
|
|
|
|
|
65 |
|
66 |
return (
|
67 |
<Card className="border-0 rounded-none">
|
@@ -69,21 +229,32 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
|
|
69 |
<div className="flex items-center gap-4">
|
70 |
<div className="w-1 h-8 bg-primary"></div>
|
71 |
<div>
|
72 |
-
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
73 |
-
|
|
|
|
|
74 |
</div>
|
75 |
</div>
|
76 |
<div className="flex gap-4">
|
77 |
{!isCalibrating ? (
|
78 |
-
<Button
|
79 |
-
|
|
|
|
|
|
|
|
|
80 |
</Button>
|
81 |
) : (
|
82 |
<Button onClick={handleFinish} variant="destructive" size="lg">
|
83 |
Finish Recording
|
84 |
</Button>
|
85 |
)}
|
86 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
87 |
<Download className="w-4 h-4 mr-2" /> Download JSON
|
88 |
</Button>
|
89 |
</div>
|
@@ -99,10 +270,21 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
|
|
99 |
</div>
|
100 |
<div className="border-t border-white/10">
|
101 |
{motorData.map(([name, data]) => (
|
102 |
-
<MotorCalibrationVisual
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
))}
|
104 |
</div>
|
105 |
</div>
|
106 |
</Card>
|
107 |
-
)
|
108 |
}
|
|
|
1 |
+
"use client";
|
2 |
+
import { useState, useMemo, useEffect, useCallback } from "react";
|
3 |
+
import { Download } from "lucide-react";
|
4 |
+
import { Button } from "@/components/ui/button";
|
5 |
+
import { Card } from "@/components/ui/card";
|
6 |
+
import { useToast } from "@/hooks/use-toast";
|
7 |
+
import {
|
8 |
+
calibrate,
|
9 |
+
releaseMotors,
|
10 |
+
type CalibrationProcess,
|
11 |
+
type LiveCalibrationData,
|
12 |
+
type WebCalibrationResults,
|
13 |
+
type RobotConnection,
|
14 |
+
} from "@lerobot/web";
|
15 |
+
import {
|
16 |
+
saveCalibrationData,
|
17 |
+
getUnifiedRobotData,
|
18 |
+
type CalibrationMetadata,
|
19 |
+
} from "@/lib/unified-storage";
|
20 |
+
import { MotorCalibrationVisual } from "@/components/motor-calibration-visual";
|
21 |
|
22 |
interface CalibrationViewProps {
|
23 |
+
robot: RobotConnection;
|
24 |
}
|
25 |
|
26 |
export function CalibrationView({ robot }: CalibrationViewProps) {
|
27 |
+
const [status, setStatus] = useState("Ready to calibrate.");
|
28 |
+
const [liveData, setLiveData] = useState<LiveCalibrationData | null>(null);
|
29 |
+
const [isCalibrating, setIsCalibrating] = useState(false);
|
30 |
+
const [isPreparing, setIsPreparing] = useState(false);
|
31 |
+
const [calibrationProcess, setCalibrationProcess] =
|
32 |
+
useState<CalibrationProcess | null>(null);
|
33 |
+
const [calibrationResults, setCalibrationResults] =
|
34 |
+
useState<WebCalibrationResults | null>(null);
|
35 |
+
const { toast } = useToast();
|
36 |
+
|
37 |
+
// Load existing calibration data from unified storage
|
38 |
+
useEffect(() => {
|
39 |
+
if (robot.serialNumber) {
|
40 |
+
const data = getUnifiedRobotData(robot.serialNumber);
|
41 |
+
if (data?.calibration) {
|
42 |
+
setCalibrationResults(data.calibration);
|
43 |
+
}
|
44 |
+
}
|
45 |
+
}, [robot.serialNumber]);
|
46 |
+
|
47 |
+
// Motor names for display
|
48 |
+
const motorNames = useMemo(
|
49 |
+
() => [
|
50 |
+
"shoulder_pan",
|
51 |
+
"shoulder_lift",
|
52 |
+
"elbow_flex",
|
53 |
+
"wrist_flex",
|
54 |
+
"wrist_roll",
|
55 |
+
"gripper",
|
56 |
+
],
|
57 |
+
[]
|
58 |
+
);
|
59 |
+
|
60 |
+
// Release motor torque before calibration
|
61 |
+
const releaseMotorTorque = useCallback(async () => {
|
62 |
+
try {
|
63 |
+
setIsPreparing(true);
|
64 |
+
setStatus("🔓 Releasing motor torque - joints can now be moved freely");
|
65 |
+
|
66 |
+
await releaseMotors(robot);
|
67 |
+
|
68 |
+
setStatus("✅ Joints are now free to move - ready to start calibration");
|
69 |
+
toast({
|
70 |
+
title: "Motors Released",
|
71 |
+
description: "Robot joints can now be moved freely for calibration",
|
72 |
+
});
|
73 |
+
} catch (error) {
|
74 |
+
console.error("Failed to release motor torque:", error);
|
75 |
+
setStatus("⚠️ Could not release motor torque - try moving joints gently");
|
76 |
+
toast({
|
77 |
+
title: "Motor Release Warning",
|
78 |
+
description:
|
79 |
+
"Could not release motor torque. Try moving joints gently.",
|
80 |
+
variant: "destructive",
|
81 |
+
});
|
82 |
+
} finally {
|
83 |
+
setIsPreparing(false);
|
84 |
+
}
|
85 |
+
}, [robot, toast]);
|
86 |
|
87 |
const handleStart = async () => {
|
88 |
+
try {
|
89 |
+
setIsCalibrating(true);
|
90 |
+
setStatus("🤖 Starting calibration process...");
|
91 |
+
|
92 |
+
// Release motors first
|
93 |
+
await releaseMotorTorque();
|
94 |
+
|
95 |
+
// Start calibration process
|
96 |
+
const process = await calibrate({
|
97 |
+
robot,
|
98 |
+
onLiveUpdate: (data) => {
|
99 |
+
setLiveData(data);
|
100 |
+
setStatus(
|
101 |
+
"📏 Recording joint ranges - move all joints through their full range"
|
102 |
+
);
|
103 |
+
},
|
104 |
+
onProgress: (message) => {
|
105 |
+
setStatus(message);
|
106 |
+
},
|
107 |
+
});
|
108 |
+
|
109 |
+
setCalibrationProcess(process);
|
110 |
+
|
111 |
+
// Add Enter key listener for stopping (matching Node.js UX)
|
112 |
+
const handleKeyPress = (event: KeyboardEvent) => {
|
113 |
+
if (event.key === "Enter") {
|
114 |
+
process.stop();
|
115 |
+
}
|
116 |
+
};
|
117 |
+
document.addEventListener("keydown", handleKeyPress);
|
118 |
+
|
119 |
+
try {
|
120 |
+
// Wait for calibration to complete
|
121 |
+
const result = await process.result;
|
122 |
+
setCalibrationResults(result);
|
123 |
+
|
124 |
+
// Save results to unified storage
|
125 |
+
if (robot.serialNumber) {
|
126 |
+
const metadata: CalibrationMetadata = {
|
127 |
+
timestamp: new Date().toISOString(),
|
128 |
+
readCount: Object.keys(liveData || {}).length > 0 ? 100 : 0,
|
129 |
+
};
|
130 |
+
|
131 |
+
// Use the result directly as WebCalibrationResults
|
132 |
+
saveCalibrationData(robot.serialNumber, result, metadata);
|
133 |
+
}
|
134 |
+
|
135 |
+
setStatus(
|
136 |
+
"✅ Calibration completed successfully! Configuration saved."
|
137 |
+
);
|
138 |
+
toast({
|
139 |
+
title: "Calibration Complete",
|
140 |
+
description: "Robot calibration has been saved successfully",
|
141 |
+
});
|
142 |
+
} finally {
|
143 |
+
document.removeEventListener("keydown", handleKeyPress);
|
144 |
+
setCalibrationProcess(null);
|
145 |
+
setIsCalibrating(false);
|
146 |
+
}
|
147 |
+
} catch (error) {
|
148 |
+
console.error("Calibration failed:", error);
|
149 |
+
setStatus(
|
150 |
+
`❌ Calibration failed: ${
|
151 |
+
error instanceof Error ? error.message : error
|
152 |
+
}`
|
153 |
+
);
|
154 |
+
toast({
|
155 |
+
title: "Calibration Failed",
|
156 |
+
description:
|
157 |
+
error instanceof Error ? error.message : "An unknown error occurred",
|
158 |
+
variant: "destructive",
|
159 |
+
});
|
160 |
+
setIsCalibrating(false);
|
161 |
+
setCalibrationProcess(null);
|
162 |
+
}
|
163 |
+
};
|
164 |
|
165 |
const handleFinish = async () => {
|
166 |
if (calibrationProcess) {
|
167 |
+
try {
|
168 |
+
calibrationProcess.stop();
|
169 |
+
toast({
|
170 |
+
title: "Calibration Stopped",
|
171 |
+
description: "Calibration recording has been stopped",
|
172 |
+
});
|
173 |
+
} catch (error) {
|
174 |
+
console.error("Failed to stop calibration:", error);
|
175 |
+
toast({
|
176 |
+
title: "Stop Error",
|
177 |
+
description: "Failed to stop calibration cleanly",
|
178 |
+
variant: "destructive",
|
179 |
+
});
|
180 |
+
}
|
181 |
}
|
182 |
+
};
|
183 |
|
184 |
const downloadJson = () => {
|
185 |
+
if (!calibrationResults) return;
|
186 |
+
|
187 |
+
try {
|
188 |
+
const dataStr =
|
189 |
+
"data:text/json;charset=utf-8," +
|
190 |
+
encodeURIComponent(JSON.stringify(calibrationResults, null, 2));
|
191 |
+
const downloadAnchorNode = document.createElement("a");
|
192 |
+
downloadAnchorNode.setAttribute("href", dataStr);
|
193 |
+
downloadAnchorNode.setAttribute(
|
194 |
+
"download",
|
195 |
+
`${robot.robotId}_calibration.json`
|
196 |
+
);
|
197 |
+
document.body.appendChild(downloadAnchorNode);
|
198 |
+
downloadAnchorNode.click();
|
199 |
+
downloadAnchorNode.remove();
|
200 |
+
|
201 |
+
toast({
|
202 |
+
title: "Download Started",
|
203 |
+
description: "Calibration file download has started",
|
204 |
+
});
|
205 |
+
} catch (error) {
|
206 |
+
console.error("Failed to download calibration file:", error);
|
207 |
+
toast({
|
208 |
+
title: "Download Error",
|
209 |
+
description: "Failed to download calibration file",
|
210 |
+
variant: "destructive",
|
211 |
+
});
|
212 |
+
}
|
213 |
+
};
|
214 |
|
215 |
const motorData = useMemo(
|
216 |
() =>
|
217 |
liveData
|
218 |
? Object.entries(liveData)
|
219 |
+
: motorNames.map((name) => [
|
220 |
+
name,
|
221 |
+
{ current: 0, min: 4095, max: 0, range: 0 },
|
222 |
+
]),
|
223 |
+
[liveData, motorNames]
|
224 |
+
);
|
225 |
|
226 |
return (
|
227 |
<Card className="border-0 rounded-none">
|
|
|
229 |
<div className="flex items-center gap-4">
|
230 |
<div className="w-1 h-8 bg-primary"></div>
|
231 |
<div>
|
232 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
233 |
+
motor calibration
|
234 |
+
</h3>
|
235 |
+
<p className="text-sm text-muted-foreground font-mono">{status}</p>
|
236 |
</div>
|
237 |
</div>
|
238 |
<div className="flex gap-4">
|
239 |
{!isCalibrating ? (
|
240 |
+
<Button
|
241 |
+
onClick={handleStart}
|
242 |
+
size="lg"
|
243 |
+
disabled={isPreparing || !robot.isConnected}
|
244 |
+
>
|
245 |
+
{isPreparing ? "Preparing..." : "Start Calibration"}
|
246 |
</Button>
|
247 |
) : (
|
248 |
<Button onClick={handleFinish} variant="destructive" size="lg">
|
249 |
Finish Recording
|
250 |
</Button>
|
251 |
)}
|
252 |
+
<Button
|
253 |
+
onClick={downloadJson}
|
254 |
+
variant="outline"
|
255 |
+
size="lg"
|
256 |
+
disabled={!calibrationResults}
|
257 |
+
>
|
258 |
<Download className="w-4 h-4 mr-2" /> Download JSON
|
259 |
</Button>
|
260 |
</div>
|
|
|
270 |
</div>
|
271 |
<div className="border-t border-white/10">
|
272 |
{motorData.map(([name, data]) => (
|
273 |
+
<MotorCalibrationVisual
|
274 |
+
key={name as string}
|
275 |
+
name={name as string}
|
276 |
+
data={
|
277 |
+
data as {
|
278 |
+
current: number;
|
279 |
+
min: number;
|
280 |
+
max: number;
|
281 |
+
range: number;
|
282 |
+
}
|
283 |
+
}
|
284 |
+
/>
|
285 |
))}
|
286 |
</div>
|
287 |
</div>
|
288 |
</Card>
|
289 |
+
);
|
290 |
}
|
examples/cyberpunk-standalone/src/components/device-dashboard.tsx
CHANGED
@@ -1,22 +1,29 @@
|
|
1 |
-
"use client"
|
2 |
|
3 |
-
import {
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
interface DeviceDashboardProps {
|
12 |
-
robots: RobotConnection[]
|
13 |
-
onCalibrate: (robot: RobotConnection) => void
|
14 |
-
onTeleoperate: (robot: RobotConnection) => void
|
15 |
-
onRemove: (robotId: string) => void
|
16 |
-
onEdit: (robot: RobotConnection) => void
|
17 |
-
onFindNew: () => void
|
18 |
-
isConnecting: boolean
|
19 |
-
onScrollToHardware: () => void
|
20 |
}
|
21 |
|
22 |
export function DeviceDashboard({
|
@@ -35,9 +42,13 @@ export function DeviceDashboard({
|
|
35 |
<div className="flex items-center gap-4">
|
36 |
<div className="w-1 h-8 bg-primary"></div>
|
37 |
<div>
|
38 |
-
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
|
|
|
|
39 |
<div className="flex items-center gap-2 mt-1">
|
40 |
-
<span className="text-xs text-muted-foreground font-mono">
|
|
|
|
|
41 |
<button
|
42 |
onClick={onScrollToHardware}
|
43 |
className="text-xs text-primary hover:text-accent transition-colors underline font-mono flex items-center gap-1"
|
@@ -49,7 +60,12 @@ export function DeviceDashboard({
|
|
49 |
</div>
|
50 |
</div>
|
51 |
{robots.length > 0 && (
|
52 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
53 |
<Plus className="w-4 h-4 mr-2" />
|
54 |
add unit
|
55 |
</Button>
|
@@ -67,17 +83,27 @@ export function DeviceDashboard({
|
|
67 |
<div className="w-16 h-16 mx-auto mb-4 border-2 border-primary/50 rounded-lg flex items-center justify-center animate-pulse">
|
68 |
<Plus className="w-8 h-8 text-primary animate-spin" />
|
69 |
</div>
|
70 |
-
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">
|
71 |
-
|
|
|
|
|
|
|
|
|
72 |
</>
|
73 |
) : (
|
74 |
<>
|
75 |
<div className="w-16 h-16 mx-auto mb-4 border-2 border-dashed border-primary/50 rounded-lg flex items-center justify-center">
|
76 |
<Plus className="w-8 h-8 text-primary/50" />
|
77 |
</div>
|
78 |
-
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">
|
|
|
|
|
79 |
|
80 |
-
<Button
|
|
|
|
|
|
|
|
|
81 |
<Plus className="w-4 h-4 mr-2" />
|
82 |
add unit
|
83 |
</Button>
|
@@ -95,18 +121,26 @@ export function DeviceDashboard({
|
|
95 |
<div className="p-4 border-b border-white/10">
|
96 |
<div className="flex items-start justify-between mb-3">
|
97 |
<div className="flex-1">
|
98 |
-
<h4 className="text-xl font-bold text-primary font-mono tracking-wider">
|
|
|
|
|
99 |
<div className="flex items-center gap-2 mt-1">
|
100 |
-
<span className="text-xs text-muted-foreground font-mono">
|
101 |
-
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
</div>
|
104 |
</div>
|
105 |
<Badge
|
106 |
variant="outline"
|
107 |
className={cn(
|
108 |
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
109 |
-
robot.isConnected && "animate-pulse-slow"
|
110 |
)}
|
111 |
>
|
112 |
{robot.isConnected ? "ONLINE" : "OFFLINE"}
|
@@ -117,14 +151,20 @@ export function DeviceDashboard({
|
|
117 |
<div className="flex-grow p-4">
|
118 |
<div className="grid grid-cols-2 gap-4 text-xs font-mono">
|
119 |
<div>
|
120 |
-
<span className="text-muted-foreground uppercase">
|
|
|
|
|
121 |
<div className="text-primary uppercase">
|
122 |
{robot.isConnected ? "operational" : "disconnected"}
|
123 |
</div>
|
124 |
</div>
|
125 |
<div>
|
126 |
-
<span className="text-muted-foreground uppercase">
|
127 |
-
|
|
|
|
|
|
|
|
|
128 |
</div>
|
129 |
</div>
|
130 |
</div>
|
@@ -157,7 +197,11 @@ export function DeviceDashboard({
|
|
157 |
<Button
|
158 |
variant="destructive"
|
159 |
size="sm"
|
160 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
161 |
className="font-mono text-xs uppercase"
|
162 |
>
|
163 |
<Trash2 className="w-3 h-3 mr-1" /> remove
|
@@ -170,5 +214,5 @@ export function DeviceDashboard({
|
|
170 |
)}
|
171 |
</div>
|
172 |
</Card>
|
173 |
-
)
|
174 |
}
|
|
|
1 |
+
"use client";
|
2 |
|
3 |
+
import {
|
4 |
+
Settings,
|
5 |
+
Gamepad2,
|
6 |
+
Trash2,
|
7 |
+
Pencil,
|
8 |
+
Plus,
|
9 |
+
ExternalLink,
|
10 |
+
} from "lucide-react";
|
11 |
+
import { Button } from "@/components/ui/button";
|
12 |
+
import { Card, CardFooter } from "@/components/ui/card";
|
13 |
+
import { Badge } from "@/components/ui/badge";
|
14 |
+
import { cn } from "@/lib/utils";
|
15 |
+
import HudCorners from "@/components/hud-corners";
|
16 |
+
import type { RobotConnection } from "@/types/robot";
|
17 |
|
18 |
interface DeviceDashboardProps {
|
19 |
+
robots: RobotConnection[];
|
20 |
+
onCalibrate: (robot: RobotConnection) => void;
|
21 |
+
onTeleoperate: (robot: RobotConnection) => void;
|
22 |
+
onRemove: (robotId: string) => void;
|
23 |
+
onEdit: (robot: RobotConnection) => void;
|
24 |
+
onFindNew: () => void;
|
25 |
+
isConnecting: boolean;
|
26 |
+
onScrollToHardware: () => void;
|
27 |
}
|
28 |
|
29 |
export function DeviceDashboard({
|
|
|
42 |
<div className="flex items-center gap-4">
|
43 |
<div className="w-1 h-8 bg-primary"></div>
|
44 |
<div>
|
45 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
46 |
+
device registry
|
47 |
+
</h3>
|
48 |
<div className="flex items-center gap-2 mt-1">
|
49 |
+
<span className="text-xs text-muted-foreground font-mono">
|
50 |
+
currently supports SO-100{" "}
|
51 |
+
</span>
|
52 |
<button
|
53 |
onClick={onScrollToHardware}
|
54 |
className="text-xs text-primary hover:text-accent transition-colors underline font-mono flex items-center gap-1"
|
|
|
60 |
</div>
|
61 |
</div>
|
62 |
{robots.length > 0 && (
|
63 |
+
<Button
|
64 |
+
onClick={onFindNew}
|
65 |
+
disabled={isConnecting}
|
66 |
+
size="lg"
|
67 |
+
className="font-mono uppercase"
|
68 |
+
>
|
69 |
<Plus className="w-4 h-4 mr-2" />
|
70 |
add unit
|
71 |
</Button>
|
|
|
83 |
<div className="w-16 h-16 mx-auto mb-4 border-2 border-primary/50 rounded-lg flex items-center justify-center animate-pulse">
|
84 |
<Plus className="w-8 h-8 text-primary animate-spin" />
|
85 |
</div>
|
86 |
+
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">
|
87 |
+
scanning for units
|
88 |
+
</h4>
|
89 |
+
<p className="text-sm text-muted-foreground mb-8">
|
90 |
+
searching for available devices...
|
91 |
+
</p>
|
92 |
</>
|
93 |
) : (
|
94 |
<>
|
95 |
<div className="w-16 h-16 mx-auto mb-4 border-2 border-dashed border-primary/50 rounded-lg flex items-center justify-center">
|
96 |
<Plus className="w-8 h-8 text-primary/50" />
|
97 |
</div>
|
98 |
+
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">
|
99 |
+
no units detected
|
100 |
+
</h4>
|
101 |
|
102 |
+
<Button
|
103 |
+
onClick={onFindNew}
|
104 |
+
size="lg"
|
105 |
+
className="font-mono uppercase"
|
106 |
+
>
|
107 |
<Plus className="w-4 h-4 mr-2" />
|
108 |
add unit
|
109 |
</Button>
|
|
|
121 |
<div className="p-4 border-b border-white/10">
|
122 |
<div className="flex items-start justify-between mb-3">
|
123 |
<div className="flex-1">
|
124 |
+
<h4 className="text-xl font-bold text-primary font-mono tracking-wider">
|
125 |
+
{robot.name}
|
126 |
+
</h4>
|
127 |
<div className="flex items-center gap-2 mt-1">
|
128 |
+
<span className="text-xs text-muted-foreground font-mono">
|
129 |
+
{robot.serialNumber}
|
130 |
+
</span>
|
131 |
+
<span className="text-xs text-muted-foreground">
|
132 |
+
•
|
133 |
+
</span>
|
134 |
+
<span className="text-xs font-mono uppercase text-muted-foreground">
|
135 |
+
{robot.robotType}
|
136 |
+
</span>
|
137 |
</div>
|
138 |
</div>
|
139 |
<Badge
|
140 |
variant="outline"
|
141 |
className={cn(
|
142 |
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
143 |
+
robot.isConnected && "animate-pulse-slow"
|
144 |
)}
|
145 |
>
|
146 |
{robot.isConnected ? "ONLINE" : "OFFLINE"}
|
|
|
151 |
<div className="flex-grow p-4">
|
152 |
<div className="grid grid-cols-2 gap-4 text-xs font-mono">
|
153 |
<div>
|
154 |
+
<span className="text-muted-foreground uppercase">
|
155 |
+
status:
|
156 |
+
</span>
|
157 |
<div className="text-primary uppercase">
|
158 |
{robot.isConnected ? "operational" : "disconnected"}
|
159 |
</div>
|
160 |
</div>
|
161 |
<div>
|
162 |
+
<span className="text-muted-foreground uppercase">
|
163 |
+
type:
|
164 |
+
</span>
|
165 |
+
<div className="text-accent uppercase">
|
166 |
+
{robot.robotType}
|
167 |
+
</div>
|
168 |
</div>
|
169 |
</div>
|
170 |
</div>
|
|
|
197 |
<Button
|
198 |
variant="destructive"
|
199 |
size="sm"
|
200 |
+
onClick={() =>
|
201 |
+
onRemove(
|
202 |
+
robot.robotId || robot.serialNumber || "unknown"
|
203 |
+
)
|
204 |
+
}
|
205 |
className="font-mono text-xs uppercase"
|
206 |
>
|
207 |
<Trash2 className="w-3 h-3 mr-1" /> remove
|
|
|
214 |
)}
|
215 |
</div>
|
216 |
</Card>
|
217 |
+
);
|
218 |
}
|
examples/cyberpunk-standalone/src/components/edit-robot-dialog.tsx
CHANGED
@@ -1,37 +1,51 @@
|
|
1 |
-
"use client"
|
2 |
-
import { useState, useEffect } from "react"
|
3 |
-
import { Button } from "@/components/ui/button"
|
4 |
-
import {
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
interface EditRobotDialogProps {
|
11 |
-
robot: RobotConnection | null
|
12 |
-
isOpen: boolean
|
13 |
-
onOpenChange: (open: boolean) => void
|
14 |
-
onSave: (updatedRobot: RobotConnection) => void
|
15 |
}
|
16 |
|
17 |
-
export function EditRobotDialog({
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
useEffect(() => {
|
22 |
if (robot) {
|
23 |
-
setName(robot.name)
|
24 |
-
setType(robot.robotType || "so100_follower")
|
25 |
}
|
26 |
-
}, [robot])
|
27 |
|
28 |
const handleSave = () => {
|
29 |
if (robot) {
|
30 |
-
onSave({ ...robot, name, robotType: type })
|
31 |
}
|
32 |
-
}
|
33 |
|
34 |
-
if (!robot) return null
|
35 |
|
36 |
return (
|
37 |
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
@@ -44,11 +58,22 @@ export function EditRobotDialog({ robot, isOpen, onOpenChange, onSave }: EditRob
|
|
44 |
<Label htmlFor="name" className="text-right">
|
45 |
Name
|
46 |
</Label>
|
47 |
-
<Input
|
|
|
|
|
|
|
|
|
|
|
48 |
</div>
|
49 |
<div className="grid grid-cols-4 items-center gap-4">
|
50 |
<Label className="text-right">Type</Label>
|
51 |
-
<RadioGroup
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
<div className="flex items-center space-x-2">
|
53 |
<RadioGroupItem value="so100_follower" id="r1" />
|
54 |
<Label htmlFor="r1">SO-100 Follower</Label>
|
@@ -74,5 +99,5 @@ export function EditRobotDialog({ robot, isOpen, onOpenChange, onSave }: EditRob
|
|
74 |
</DialogFooter>
|
75 |
</DialogContent>
|
76 |
</Dialog>
|
77 |
-
)
|
78 |
}
|
|
|
1 |
+
"use client";
|
2 |
+
import { useState, useEffect } from "react";
|
3 |
+
import { Button } from "@/components/ui/button";
|
4 |
+
import {
|
5 |
+
Dialog,
|
6 |
+
DialogContent,
|
7 |
+
DialogHeader,
|
8 |
+
DialogTitle,
|
9 |
+
DialogFooter,
|
10 |
+
DialogClose,
|
11 |
+
} from "@/components/ui/dialog";
|
12 |
+
import { Input } from "@/components/ui/input";
|
13 |
+
import { Label } from "@/components/ui/label";
|
14 |
+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
15 |
+
import type { RobotConnection } from "@/types/robot";
|
16 |
|
17 |
interface EditRobotDialogProps {
|
18 |
+
robot: RobotConnection | null;
|
19 |
+
isOpen: boolean;
|
20 |
+
onOpenChange: (open: boolean) => void;
|
21 |
+
onSave: (updatedRobot: RobotConnection) => void;
|
22 |
}
|
23 |
|
24 |
+
export function EditRobotDialog({
|
25 |
+
robot,
|
26 |
+
isOpen,
|
27 |
+
onOpenChange,
|
28 |
+
onSave,
|
29 |
+
}: EditRobotDialogProps) {
|
30 |
+
const [name, setName] = useState("");
|
31 |
+
const [type, setType] = useState<"so100_follower" | "so100_leader">(
|
32 |
+
"so100_follower"
|
33 |
+
);
|
34 |
|
35 |
useEffect(() => {
|
36 |
if (robot) {
|
37 |
+
setName(robot.name);
|
38 |
+
setType(robot.robotType || "so100_follower");
|
39 |
}
|
40 |
+
}, [robot]);
|
41 |
|
42 |
const handleSave = () => {
|
43 |
if (robot) {
|
44 |
+
onSave({ ...robot, name, robotId: name, robotType: type });
|
45 |
}
|
46 |
+
};
|
47 |
|
48 |
+
if (!robot) return null;
|
49 |
|
50 |
return (
|
51 |
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
|
58 |
<Label htmlFor="name" className="text-right">
|
59 |
Name
|
60 |
</Label>
|
61 |
+
<Input
|
62 |
+
id="name"
|
63 |
+
value={name}
|
64 |
+
onChange={(e) => setName(e.target.value)}
|
65 |
+
className="col-span-3 font-mono"
|
66 |
+
/>
|
67 |
</div>
|
68 |
<div className="grid grid-cols-4 items-center gap-4">
|
69 |
<Label className="text-right">Type</Label>
|
70 |
+
<RadioGroup
|
71 |
+
value={type}
|
72 |
+
onValueChange={(value) =>
|
73 |
+
setType(value as "so100_follower" | "so100_leader")
|
74 |
+
}
|
75 |
+
className="col-span-3"
|
76 |
+
>
|
77 |
<div className="flex items-center space-x-2">
|
78 |
<RadioGroupItem value="so100_follower" id="r1" />
|
79 |
<Label htmlFor="r1">SO-100 Follower</Label>
|
|
|
99 |
</DialogFooter>
|
100 |
</DialogContent>
|
101 |
</Dialog>
|
102 |
+
);
|
103 |
}
|
examples/cyberpunk-standalone/src/components/teleoperation-view.tsx
CHANGED
@@ -1,73 +1,396 @@
|
|
1 |
-
"use client"
|
2 |
-
import { useState, useEffect, useMemo } from "react"
|
3 |
-
import { Power, PowerOff, Keyboard } from "lucide-react"
|
4 |
-
import { Button } from "@/components/ui/button"
|
5 |
-
import { Card } from "@/components/ui/card"
|
6 |
-
import { Badge } from "@/components/ui/badge"
|
7 |
-
import { Slider } from "@/components/ui/slider"
|
8 |
-
import {
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
interface TeleoperationViewProps {
|
16 |
-
robot: RobotConnection
|
17 |
}
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
20 |
-
const [
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
|
|
29 |
const calibrationData = useMemo(() => {
|
30 |
-
if (
|
31 |
-
return lerobot.MOCK_MOTOR_NAMES.reduce((acc, name) => {
|
32 |
-
acc[name] = { min: 1000, max: 3000 }
|
33 |
-
return acc
|
34 |
-
}, {} as WebCalibrationResults)
|
35 |
-
}, [localCalibrationData])
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
process = await lerobot.teleoperate(robot, {
|
41 |
-
calibrationData,
|
42 |
-
onStateUpdate: setTeleopState,
|
43 |
-
})
|
44 |
-
setTeleopProcess(process)
|
45 |
}
|
46 |
-
setup()
|
47 |
|
48 |
-
|
49 |
-
|
|
|
50 |
|
51 |
-
|
52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
return () => {
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
}
|
59 |
-
}
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
72 |
return (
|
73 |
<Card className="border-0 rounded-none">
|
@@ -76,30 +399,40 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
76 |
<div className="flex items-center gap-4">
|
77 |
<div className="w-1 h-8 bg-primary"></div>
|
78 |
<div>
|
79 |
-
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
|
|
|
|
80 |
<p className="text-sm text-muted-foreground font-mono">
|
81 |
-
manual
|
|
|
|
|
82 |
</p>
|
83 |
</div>
|
84 |
</div>
|
85 |
<div className="flex items-center gap-6">
|
86 |
<div className="border-l border-white/10 pl-6 flex items-center gap-4">
|
87 |
{teleopState?.isActive ? (
|
88 |
-
<Button onClick={
|
89 |
<PowerOff className="w-5 h-5 mr-2" /> Stop Control
|
90 |
</Button>
|
91 |
) : (
|
92 |
-
<Button
|
|
|
|
|
|
|
|
|
93 |
<Power className="w-5 h-5 mr-2" /> Control Robot
|
94 |
</Button>
|
95 |
)}
|
96 |
<div className="flex items-center gap-2">
|
97 |
-
<span className="text-sm font-mono text-muted-foreground uppercase">
|
|
|
|
|
98 |
<Badge
|
99 |
variant="outline"
|
100 |
className={cn(
|
101 |
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
102 |
-
teleopState?.isActive && "animate-pulse-slow"
|
103 |
)}
|
104 |
>
|
105 |
{teleopState?.isActive ? "ACTIVE" : "STOPPED"}
|
@@ -111,21 +444,33 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
111 |
</div>
|
112 |
<div className="pt-6 p-6 grid md:grid-cols-2 gap-8">
|
113 |
<div>
|
114 |
-
<h3 className="font-sans font-semibold mb-4 text-xl">
|
|
|
|
|
115 |
<div className="space-y-6">
|
116 |
{motorConfigs.map((motor) => (
|
117 |
<div key={motor.name}>
|
118 |
-
<label className="text-sm font-mono text-muted-foreground">
|
|
|
|
|
119 |
<div className="flex items-center gap-4">
|
120 |
<Slider
|
121 |
value={[motor.currentPosition]}
|
122 |
min={motor.minPosition}
|
123 |
max={motor.maxPosition}
|
124 |
step={1}
|
125 |
-
onValueChange={(val) =>
|
126 |
disabled={!teleopState?.isActive}
|
|
|
127 |
/>
|
128 |
-
<span
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
{Math.round(motor.currentPosition)}
|
130 |
</span>
|
131 |
</div>
|
@@ -134,30 +479,68 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
134 |
</div>
|
135 |
</div>
|
136 |
<div>
|
137 |
-
<h3 className="font-sans font-semibold mb-4 text-xl">
|
|
|
|
|
138 |
<div className="p-4 bg-black/30 rounded-lg space-y-4">
|
139 |
<div className="flex justify-around items-end">
|
140 |
<div className="flex flex-col items-center gap-2">
|
141 |
<VirtualKey
|
142 |
label="↑"
|
143 |
subLabel="Lift+"
|
144 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
/>
|
146 |
<div className="flex gap-2">
|
147 |
<VirtualKey
|
148 |
label="←"
|
149 |
subLabel="Pan-"
|
150 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
/>
|
152 |
<VirtualKey
|
153 |
label="↓"
|
154 |
subLabel="Lift-"
|
155 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
/>
|
157 |
<VirtualKey
|
158 |
label="→"
|
159 |
subLabel="Pan+"
|
160 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
/>
|
162 |
</div>
|
163 |
<span className="font-bold text-sm font-sans">Shoulder</span>
|
@@ -167,22 +550,56 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
167 |
label="W"
|
168 |
subLabel="Elbow+"
|
169 |
isPressed={!!keyStates[controls.elbow_flex.positive]?.pressed}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
/>
|
171 |
<div className="flex gap-2">
|
172 |
<VirtualKey
|
173 |
label="A"
|
174 |
subLabel="Wrist+"
|
175 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
/>
|
177 |
<VirtualKey
|
178 |
label="S"
|
179 |
subLabel="Elbow-"
|
180 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
/>
|
182 |
<VirtualKey
|
183 |
label="D"
|
184 |
subLabel="Wrist-"
|
185 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
/>
|
187 |
</div>
|
188 |
<span className="font-bold text-sm font-sans">Elbow/Wrist</span>
|
@@ -192,17 +609,57 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
192 |
<VirtualKey
|
193 |
label="Q"
|
194 |
subLabel="Roll+"
|
195 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
/>
|
197 |
<VirtualKey
|
198 |
label="E"
|
199 |
subLabel="Roll-"
|
200 |
-
isPressed={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
/>
|
202 |
</div>
|
203 |
<div className="flex gap-2">
|
204 |
-
<VirtualKey
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
</div>
|
207 |
<span className="font-bold text-sm font-sans">Roll/Grip</span>
|
208 |
</div>
|
@@ -211,7 +668,10 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
211 |
<div className="flex justify-between items-center font-mono text-sm">
|
212 |
<div className="flex items-center gap-2 text-muted-foreground">
|
213 |
<Keyboard className="w-4 h-4" />
|
214 |
-
<span>
|
|
|
|
|
|
|
215 |
</div>
|
216 |
<TooltipProvider>
|
217 |
<Tooltip>
|
@@ -219,10 +679,33 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
219 |
<div
|
220 |
className={cn(
|
221 |
"w-10 h-6 border rounded-md flex items-center justify-center font-mono text-xs transition-all",
|
222 |
-
|
|
|
|
|
|
|
|
|
|
|
223 |
? "bg-destructive text-destructive-foreground border-destructive"
|
224 |
-
: "bg-background"
|
225 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
226 |
>
|
227 |
ESC
|
228 |
</div>
|
@@ -236,5 +719,5 @@ export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
|
236 |
</div>
|
237 |
</div>
|
238 |
</Card>
|
239 |
-
)
|
240 |
}
|
|
|
1 |
+
"use client";
|
2 |
+
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
3 |
+
import { Power, PowerOff, Keyboard } from "lucide-react";
|
4 |
+
import { Button } from "@/components/ui/button";
|
5 |
+
import { Card } from "@/components/ui/card";
|
6 |
+
import { Badge } from "@/components/ui/badge";
|
7 |
+
import { Slider } from "@/components/ui/slider";
|
8 |
+
import {
|
9 |
+
Tooltip,
|
10 |
+
TooltipContent,
|
11 |
+
TooltipProvider,
|
12 |
+
TooltipTrigger,
|
13 |
+
} from "@/components/ui/tooltip";
|
14 |
+
import { cn } from "@/lib/utils";
|
15 |
+
import { useToast } from "@/hooks/use-toast";
|
16 |
+
import {
|
17 |
+
teleoperate,
|
18 |
+
type TeleoperationProcess,
|
19 |
+
type TeleoperationState,
|
20 |
+
type TeleoperateConfig,
|
21 |
+
type RobotConnection,
|
22 |
+
} from "@lerobot/web";
|
23 |
+
import { getUnifiedRobotData } from "@/lib/unified-storage";
|
24 |
+
import VirtualKey from "@/components/VirtualKey";
|
25 |
|
26 |
interface TeleoperationViewProps {
|
27 |
+
robot: RobotConnection;
|
28 |
}
|
29 |
|
30 |
+
// Keyboard controls for SO-100 (from conventions)
|
31 |
+
const SO100_KEYBOARD_CONTROLS = {
|
32 |
+
shoulder_pan: { positive: "ArrowRight", negative: "ArrowLeft" },
|
33 |
+
shoulder_lift: { positive: "ArrowUp", negative: "ArrowDown" },
|
34 |
+
elbow_flex: { positive: "w", negative: "s" },
|
35 |
+
wrist_flex: { positive: "a", negative: "d" },
|
36 |
+
wrist_roll: { positive: "q", negative: "e" },
|
37 |
+
gripper: { positive: "o", negative: "c" },
|
38 |
+
stop: "Escape",
|
39 |
+
};
|
40 |
+
|
41 |
+
// Default motor configurations for immediate display
|
42 |
+
const DEFAULT_MOTOR_CONFIGS = [
|
43 |
+
{
|
44 |
+
name: "shoulder_pan",
|
45 |
+
currentPosition: 2048,
|
46 |
+
minPosition: 0,
|
47 |
+
maxPosition: 4095,
|
48 |
+
},
|
49 |
+
{
|
50 |
+
name: "shoulder_lift",
|
51 |
+
currentPosition: 2048,
|
52 |
+
minPosition: 0,
|
53 |
+
maxPosition: 4095,
|
54 |
+
},
|
55 |
+
{
|
56 |
+
name: "elbow_flex",
|
57 |
+
currentPosition: 2048,
|
58 |
+
minPosition: 0,
|
59 |
+
maxPosition: 4095,
|
60 |
+
},
|
61 |
+
{
|
62 |
+
name: "wrist_flex",
|
63 |
+
currentPosition: 2048,
|
64 |
+
minPosition: 0,
|
65 |
+
maxPosition: 4095,
|
66 |
+
},
|
67 |
+
{
|
68 |
+
name: "wrist_roll",
|
69 |
+
currentPosition: 2048,
|
70 |
+
minPosition: 0,
|
71 |
+
maxPosition: 4095,
|
72 |
+
},
|
73 |
+
{ name: "gripper", currentPosition: 2048, minPosition: 0, maxPosition: 4095 },
|
74 |
+
];
|
75 |
+
|
76 |
export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
77 |
+
const [teleopState, setTeleopState] = useState<TeleoperationState>({
|
78 |
+
isActive: false,
|
79 |
+
motorConfigs: [],
|
80 |
+
lastUpdate: 0,
|
81 |
+
keyStates: {},
|
82 |
+
});
|
83 |
+
|
84 |
+
const [isInitialized, setIsInitialized] = useState(false);
|
85 |
+
// Local slider positions for immediate UI feedback with timestamps
|
86 |
+
const [localMotorPositions, setLocalMotorPositions] = useState<{
|
87 |
+
[motorName: string]: { position: number; timestamp: number };
|
88 |
+
}>({});
|
89 |
+
const keyboardProcessRef = useRef<TeleoperationProcess | null>(null);
|
90 |
+
const directProcessRef = useRef<TeleoperationProcess | null>(null);
|
91 |
+
const { toast } = useToast();
|
92 |
|
93 |
+
// Load calibration data from unified storage
|
94 |
const calibrationData = useMemo(() => {
|
95 |
+
if (!robot.serialNumber) return undefined;
|
|
|
|
|
|
|
|
|
|
|
96 |
|
97 |
+
const data = getUnifiedRobotData(robot.serialNumber);
|
98 |
+
if (data?.calibration) {
|
99 |
+
return data.calibration;
|
|
|
|
|
|
|
|
|
|
|
100 |
}
|
|
|
101 |
|
102 |
+
// Return undefined if no calibration data - let library handle defaults
|
103 |
+
return undefined;
|
104 |
+
}, [robot.serialNumber]);
|
105 |
|
106 |
+
// Lazy initialization function - only connects when user wants to start
|
107 |
+
const initializeTeleoperation = async () => {
|
108 |
+
if (!robot || !robot.robotType) {
|
109 |
+
return false;
|
110 |
+
}
|
111 |
+
|
112 |
+
try {
|
113 |
+
// Create keyboard teleoperation process
|
114 |
+
const keyboardConfig: TeleoperateConfig = {
|
115 |
+
robot: robot,
|
116 |
+
teleop: {
|
117 |
+
type: "keyboard",
|
118 |
+
},
|
119 |
+
calibrationData,
|
120 |
+
onStateUpdate: (state: TeleoperationState) => {
|
121 |
+
setTeleopState(state);
|
122 |
+
},
|
123 |
+
};
|
124 |
+
const keyboardProcess = await teleoperate(keyboardConfig);
|
125 |
|
126 |
+
// Create direct teleoperation process
|
127 |
+
const directConfig: TeleoperateConfig = {
|
128 |
+
robot: robot,
|
129 |
+
teleop: {
|
130 |
+
type: "direct",
|
131 |
+
},
|
132 |
+
calibrationData,
|
133 |
+
};
|
134 |
+
const directProcess = await teleoperate(directConfig);
|
135 |
+
|
136 |
+
keyboardProcessRef.current = keyboardProcess;
|
137 |
+
directProcessRef.current = directProcess;
|
138 |
+
setTeleopState(keyboardProcess.getState());
|
139 |
+
|
140 |
+
// Initialize local motor positions from hardware state
|
141 |
+
const initialState = keyboardProcess.getState();
|
142 |
+
const initialPositions: {
|
143 |
+
[motorName: string]: { position: number; timestamp: number };
|
144 |
+
} = {};
|
145 |
+
initialState.motorConfigs.forEach((motor) => {
|
146 |
+
initialPositions[motor.name] = {
|
147 |
+
position: motor.currentPosition,
|
148 |
+
timestamp: Date.now(),
|
149 |
+
};
|
150 |
+
});
|
151 |
+
setLocalMotorPositions(initialPositions);
|
152 |
+
|
153 |
+
setIsInitialized(true);
|
154 |
+
|
155 |
+
return true;
|
156 |
+
} catch (error) {
|
157 |
+
const errorMessage =
|
158 |
+
error instanceof Error
|
159 |
+
? error.message
|
160 |
+
: "Failed to initialize teleoperation";
|
161 |
+
toast({
|
162 |
+
title: "Teleoperation Error",
|
163 |
+
description: errorMessage,
|
164 |
+
variant: "destructive",
|
165 |
+
});
|
166 |
+
return false;
|
167 |
+
}
|
168 |
+
};
|
169 |
+
|
170 |
+
// Cleanup on unmount
|
171 |
+
useEffect(() => {
|
172 |
return () => {
|
173 |
+
const cleanup = async () => {
|
174 |
+
try {
|
175 |
+
if (keyboardProcessRef.current) {
|
176 |
+
await keyboardProcessRef.current.disconnect();
|
177 |
+
keyboardProcessRef.current = null;
|
178 |
+
}
|
179 |
+
if (directProcessRef.current) {
|
180 |
+
await directProcessRef.current.disconnect();
|
181 |
+
directProcessRef.current = null;
|
182 |
+
}
|
183 |
+
} catch (error) {
|
184 |
+
console.warn("Error during teleoperation cleanup:", error);
|
185 |
+
}
|
186 |
+
};
|
187 |
+
cleanup();
|
188 |
+
};
|
189 |
+
}, []);
|
190 |
+
|
191 |
+
// Keyboard event handlers
|
192 |
+
const handleKeyDown = useCallback(
|
193 |
+
(event: KeyboardEvent) => {
|
194 |
+
if (!teleopState.isActive || !keyboardProcessRef.current) return;
|
195 |
+
|
196 |
+
const key = event.key;
|
197 |
+
event.preventDefault();
|
198 |
+
|
199 |
+
const keyboardTeleoperator = keyboardProcessRef.current.teleoperator;
|
200 |
+
if (keyboardTeleoperator && "updateKeyState" in keyboardTeleoperator) {
|
201 |
+
(
|
202 |
+
keyboardTeleoperator as {
|
203 |
+
updateKeyState: (key: string, pressed: boolean) => void;
|
204 |
+
}
|
205 |
+
).updateKeyState(key, true);
|
206 |
+
}
|
207 |
+
},
|
208 |
+
[teleopState.isActive]
|
209 |
+
);
|
210 |
+
|
211 |
+
const handleKeyUp = useCallback(
|
212 |
+
(event: KeyboardEvent) => {
|
213 |
+
if (!teleopState.isActive || !keyboardProcessRef.current) return;
|
214 |
+
|
215 |
+
const key = event.key;
|
216 |
+
event.preventDefault();
|
217 |
+
|
218 |
+
const keyboardTeleoperator = keyboardProcessRef.current.teleoperator;
|
219 |
+
if (keyboardTeleoperator && "updateKeyState" in keyboardTeleoperator) {
|
220 |
+
(
|
221 |
+
keyboardTeleoperator as {
|
222 |
+
updateKeyState: (key: string, pressed: boolean) => void;
|
223 |
+
}
|
224 |
+
).updateKeyState(key, false);
|
225 |
+
}
|
226 |
+
},
|
227 |
+
[teleopState.isActive]
|
228 |
+
);
|
229 |
+
|
230 |
+
// Register keyboard events
|
231 |
+
useEffect(() => {
|
232 |
+
if (teleopState.isActive) {
|
233 |
+
window.addEventListener("keydown", handleKeyDown);
|
234 |
+
window.addEventListener("keyup", handleKeyUp);
|
235 |
+
|
236 |
+
return () => {
|
237 |
+
window.removeEventListener("keydown", handleKeyDown);
|
238 |
+
window.removeEventListener("keyup", handleKeyUp);
|
239 |
+
};
|
240 |
+
}
|
241 |
+
}, [teleopState.isActive, handleKeyDown, handleKeyUp]);
|
242 |
+
|
243 |
+
const handleStart = async () => {
|
244 |
+
// Initialize on first use if not already initialized
|
245 |
+
if (!isInitialized) {
|
246 |
+
const success = await initializeTeleoperation();
|
247 |
+
if (!success) return;
|
248 |
+
}
|
249 |
+
|
250 |
+
if (!keyboardProcessRef.current || !directProcessRef.current) {
|
251 |
+
toast({
|
252 |
+
title: "Teleoperation Error",
|
253 |
+
description: "Teleoperation not initialized",
|
254 |
+
variant: "destructive",
|
255 |
+
});
|
256 |
+
return;
|
257 |
+
}
|
258 |
+
|
259 |
+
try {
|
260 |
+
keyboardProcessRef.current.start();
|
261 |
+
directProcessRef.current.start();
|
262 |
+
} catch (error) {
|
263 |
+
const errorMessage =
|
264 |
+
error instanceof Error
|
265 |
+
? error.message
|
266 |
+
: "Failed to start teleoperation";
|
267 |
+
toast({
|
268 |
+
title: "Start Error",
|
269 |
+
description: errorMessage,
|
270 |
+
variant: "destructive",
|
271 |
+
});
|
272 |
+
}
|
273 |
+
};
|
274 |
+
|
275 |
+
const handleStop = async () => {
|
276 |
+
try {
|
277 |
+
if (keyboardProcessRef.current) {
|
278 |
+
keyboardProcessRef.current.stop();
|
279 |
+
}
|
280 |
+
if (directProcessRef.current) {
|
281 |
+
directProcessRef.current.stop();
|
282 |
+
}
|
283 |
+
} catch (error) {
|
284 |
+
console.warn("Error during teleoperation stop:", error);
|
285 |
}
|
286 |
+
};
|
287 |
+
|
288 |
+
// Virtual keyboard functions
|
289 |
+
const simulateKeyPress = (key: string) => {
|
290 |
+
if (!keyboardProcessRef.current || !teleopState.isActive) return;
|
291 |
+
|
292 |
+
const keyboardTeleoperator = keyboardProcessRef.current.teleoperator;
|
293 |
+
if (keyboardTeleoperator && "updateKeyState" in keyboardTeleoperator) {
|
294 |
+
(
|
295 |
+
keyboardTeleoperator as {
|
296 |
+
updateKeyState: (key: string, pressed: boolean) => void;
|
297 |
+
}
|
298 |
+
).updateKeyState(key, true);
|
299 |
+
}
|
300 |
+
};
|
301 |
+
|
302 |
+
const simulateKeyRelease = (key: string) => {
|
303 |
+
if (!keyboardProcessRef.current || !teleopState.isActive) return;
|
304 |
+
|
305 |
+
const keyboardTeleoperator = keyboardProcessRef.current.teleoperator;
|
306 |
+
if (keyboardTeleoperator && "updateKeyState" in keyboardTeleoperator) {
|
307 |
+
(
|
308 |
+
keyboardTeleoperator as {
|
309 |
+
updateKeyState: (key: string, pressed: boolean) => void;
|
310 |
+
}
|
311 |
+
).updateKeyState(key, false);
|
312 |
+
}
|
313 |
+
};
|
314 |
+
|
315 |
+
// Motor control through direct teleoperator
|
316 |
+
const moveMotor = async (motorName: string, position: number) => {
|
317 |
+
if (!directProcessRef.current) return;
|
318 |
+
|
319 |
+
try {
|
320 |
+
// Immediately update local UI state for responsive slider feedback
|
321 |
+
setLocalMotorPositions((prev) => ({
|
322 |
+
...prev,
|
323 |
+
[motorName]: { position, timestamp: Date.now() },
|
324 |
+
}));
|
325 |
+
|
326 |
+
const directTeleoperator = directProcessRef.current.teleoperator;
|
327 |
+
if (directTeleoperator && "moveMotor" in directTeleoperator) {
|
328 |
+
await (
|
329 |
+
directTeleoperator as {
|
330 |
+
moveMotor: (motorName: string, position: number) => Promise<void>;
|
331 |
+
}
|
332 |
+
).moveMotor(motorName, position);
|
333 |
+
}
|
334 |
+
} catch (error) {
|
335 |
+
console.warn(
|
336 |
+
`Failed to move motor ${motorName} to position ${position}:`,
|
337 |
+
error
|
338 |
+
);
|
339 |
+
toast({
|
340 |
+
title: "Motor Control Error",
|
341 |
+
description: `Failed to move ${motorName}`,
|
342 |
+
variant: "destructive",
|
343 |
+
});
|
344 |
+
}
|
345 |
+
};
|
346 |
+
|
347 |
+
// Merge hardware state with local UI state for responsive sliders
|
348 |
+
const motorConfigs = useMemo(() => {
|
349 |
+
const realMotorConfigs = teleopState?.motorConfigs || [];
|
350 |
+
const now = Date.now();
|
351 |
+
|
352 |
+
// If we have real motor configs, use them with local position overrides when recent
|
353 |
+
if (realMotorConfigs.length > 0) {
|
354 |
+
return realMotorConfigs.map((motor) => {
|
355 |
+
const localData = localMotorPositions[motor.name];
|
356 |
+
// Use local position only if it's very recent (within 100ms), otherwise use hardware position
|
357 |
+
const useLocalPosition = localData && now - localData.timestamp < 100;
|
358 |
+
return {
|
359 |
+
...motor,
|
360 |
+
currentPosition: useLocalPosition
|
361 |
+
? localData.position
|
362 |
+
: motor.currentPosition,
|
363 |
+
};
|
364 |
+
});
|
365 |
+
}
|
366 |
+
|
367 |
+
// Otherwise, show default configs with calibration data if available
|
368 |
+
return DEFAULT_MOTOR_CONFIGS.map((motor) => {
|
369 |
+
const calibratedMotor = calibrationData?.[motor.name];
|
370 |
+
const localData = localMotorPositions[motor.name];
|
371 |
+
const useLocalPosition = localData && now - localData.timestamp < 100;
|
372 |
+
|
373 |
+
return {
|
374 |
+
...motor,
|
375 |
+
minPosition: calibratedMotor?.range_min ?? motor.minPosition,
|
376 |
+
maxPosition: calibratedMotor?.range_max ?? motor.maxPosition,
|
377 |
+
// Show 0 when inactive to look deactivated, local/real position when active
|
378 |
+
currentPosition: teleopState?.isActive
|
379 |
+
? useLocalPosition
|
380 |
+
? localData.position
|
381 |
+
: motor.currentPosition
|
382 |
+
: 0,
|
383 |
+
};
|
384 |
+
});
|
385 |
+
}, [
|
386 |
+
teleopState?.motorConfigs,
|
387 |
+
teleopState?.isActive,
|
388 |
+
localMotorPositions,
|
389 |
+
calibrationData,
|
390 |
+
]);
|
391 |
+
|
392 |
+
const keyStates = teleopState?.keyStates || {};
|
393 |
+
const controls = SO100_KEYBOARD_CONTROLS;
|
394 |
|
395 |
return (
|
396 |
<Card className="border-0 rounded-none">
|
|
|
399 |
<div className="flex items-center gap-4">
|
400 |
<div className="w-1 h-8 bg-primary"></div>
|
401 |
<div>
|
402 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">
|
403 |
+
robot control
|
404 |
+
</h3>
|
405 |
<p className="text-sm text-muted-foreground font-mono">
|
406 |
+
manual{" "}
|
407 |
+
<span className="text-muted-foreground">teleoperate</span>{" "}
|
408 |
+
interface
|
409 |
</p>
|
410 |
</div>
|
411 |
</div>
|
412 |
<div className="flex items-center gap-6">
|
413 |
<div className="border-l border-white/10 pl-6 flex items-center gap-4">
|
414 |
{teleopState?.isActive ? (
|
415 |
+
<Button onClick={handleStop} variant="destructive" size="lg">
|
416 |
<PowerOff className="w-5 h-5 mr-2" /> Stop Control
|
417 |
</Button>
|
418 |
) : (
|
419 |
+
<Button
|
420 |
+
onClick={handleStart}
|
421 |
+
size="lg"
|
422 |
+
disabled={!robot.isConnected}
|
423 |
+
>
|
424 |
<Power className="w-5 h-5 mr-2" /> Control Robot
|
425 |
</Button>
|
426 |
)}
|
427 |
<div className="flex items-center gap-2">
|
428 |
+
<span className="text-sm font-mono text-muted-foreground uppercase">
|
429 |
+
status:
|
430 |
+
</span>
|
431 |
<Badge
|
432 |
variant="outline"
|
433 |
className={cn(
|
434 |
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
435 |
+
teleopState?.isActive && "animate-pulse-slow"
|
436 |
)}
|
437 |
>
|
438 |
{teleopState?.isActive ? "ACTIVE" : "STOPPED"}
|
|
|
444 |
</div>
|
445 |
<div className="pt-6 p-6 grid md:grid-cols-2 gap-8">
|
446 |
<div>
|
447 |
+
<h3 className="font-sans font-semibold mb-4 text-xl">
|
448 |
+
Motor Control
|
449 |
+
</h3>
|
450 |
<div className="space-y-6">
|
451 |
{motorConfigs.map((motor) => (
|
452 |
<div key={motor.name}>
|
453 |
+
<label className="text-sm font-mono text-muted-foreground">
|
454 |
+
{motor.name}
|
455 |
+
</label>
|
456 |
<div className="flex items-center gap-4">
|
457 |
<Slider
|
458 |
value={[motor.currentPosition]}
|
459 |
min={motor.minPosition}
|
460 |
max={motor.maxPosition}
|
461 |
step={1}
|
462 |
+
onValueChange={(val) => moveMotor(motor.name, val[0])}
|
463 |
disabled={!teleopState?.isActive}
|
464 |
+
className={!teleopState?.isActive ? "opacity-50" : ""}
|
465 |
/>
|
466 |
+
<span
|
467 |
+
className={cn(
|
468 |
+
"text-lg font-mono w-16 text-right",
|
469 |
+
teleopState?.isActive
|
470 |
+
? "text-accent"
|
471 |
+
: "text-muted-foreground"
|
472 |
+
)}
|
473 |
+
>
|
474 |
{Math.round(motor.currentPosition)}
|
475 |
</span>
|
476 |
</div>
|
|
|
479 |
</div>
|
480 |
</div>
|
481 |
<div>
|
482 |
+
<h3 className="font-sans font-semibold mb-4 text-xl">
|
483 |
+
Keyboard Layout & Status
|
484 |
+
</h3>
|
485 |
<div className="p-4 bg-black/30 rounded-lg space-y-4">
|
486 |
<div className="flex justify-around items-end">
|
487 |
<div className="flex flex-col items-center gap-2">
|
488 |
<VirtualKey
|
489 |
label="↑"
|
490 |
subLabel="Lift+"
|
491 |
+
isPressed={
|
492 |
+
!!keyStates[controls.shoulder_lift.positive]?.pressed
|
493 |
+
}
|
494 |
+
onMouseDown={() =>
|
495 |
+
simulateKeyPress(controls.shoulder_lift.positive)
|
496 |
+
}
|
497 |
+
onMouseUp={() =>
|
498 |
+
simulateKeyRelease(controls.shoulder_lift.positive)
|
499 |
+
}
|
500 |
+
disabled={!teleopState?.isActive}
|
501 |
/>
|
502 |
<div className="flex gap-2">
|
503 |
<VirtualKey
|
504 |
label="←"
|
505 |
subLabel="Pan-"
|
506 |
+
isPressed={
|
507 |
+
!!keyStates[controls.shoulder_pan.negative]?.pressed
|
508 |
+
}
|
509 |
+
onMouseDown={() =>
|
510 |
+
simulateKeyPress(controls.shoulder_pan.negative)
|
511 |
+
}
|
512 |
+
onMouseUp={() =>
|
513 |
+
simulateKeyRelease(controls.shoulder_pan.negative)
|
514 |
+
}
|
515 |
+
disabled={!teleopState?.isActive}
|
516 |
/>
|
517 |
<VirtualKey
|
518 |
label="↓"
|
519 |
subLabel="Lift-"
|
520 |
+
isPressed={
|
521 |
+
!!keyStates[controls.shoulder_lift.negative]?.pressed
|
522 |
+
}
|
523 |
+
onMouseDown={() =>
|
524 |
+
simulateKeyPress(controls.shoulder_lift.negative)
|
525 |
+
}
|
526 |
+
onMouseUp={() =>
|
527 |
+
simulateKeyRelease(controls.shoulder_lift.negative)
|
528 |
+
}
|
529 |
+
disabled={!teleopState?.isActive}
|
530 |
/>
|
531 |
<VirtualKey
|
532 |
label="→"
|
533 |
subLabel="Pan+"
|
534 |
+
isPressed={
|
535 |
+
!!keyStates[controls.shoulder_pan.positive]?.pressed
|
536 |
+
}
|
537 |
+
onMouseDown={() =>
|
538 |
+
simulateKeyPress(controls.shoulder_pan.positive)
|
539 |
+
}
|
540 |
+
onMouseUp={() =>
|
541 |
+
simulateKeyRelease(controls.shoulder_pan.positive)
|
542 |
+
}
|
543 |
+
disabled={!teleopState?.isActive}
|
544 |
/>
|
545 |
</div>
|
546 |
<span className="font-bold text-sm font-sans">Shoulder</span>
|
|
|
550 |
label="W"
|
551 |
subLabel="Elbow+"
|
552 |
isPressed={!!keyStates[controls.elbow_flex.positive]?.pressed}
|
553 |
+
onMouseDown={() =>
|
554 |
+
simulateKeyPress(controls.elbow_flex.positive)
|
555 |
+
}
|
556 |
+
onMouseUp={() =>
|
557 |
+
simulateKeyRelease(controls.elbow_flex.positive)
|
558 |
+
}
|
559 |
+
disabled={!teleopState?.isActive}
|
560 |
/>
|
561 |
<div className="flex gap-2">
|
562 |
<VirtualKey
|
563 |
label="A"
|
564 |
subLabel="Wrist+"
|
565 |
+
isPressed={
|
566 |
+
!!keyStates[controls.wrist_flex.positive]?.pressed
|
567 |
+
}
|
568 |
+
onMouseDown={() =>
|
569 |
+
simulateKeyPress(controls.wrist_flex.positive)
|
570 |
+
}
|
571 |
+
onMouseUp={() =>
|
572 |
+
simulateKeyRelease(controls.wrist_flex.positive)
|
573 |
+
}
|
574 |
+
disabled={!teleopState?.isActive}
|
575 |
/>
|
576 |
<VirtualKey
|
577 |
label="S"
|
578 |
subLabel="Elbow-"
|
579 |
+
isPressed={
|
580 |
+
!!keyStates[controls.elbow_flex.negative]?.pressed
|
581 |
+
}
|
582 |
+
onMouseDown={() =>
|
583 |
+
simulateKeyPress(controls.elbow_flex.negative)
|
584 |
+
}
|
585 |
+
onMouseUp={() =>
|
586 |
+
simulateKeyRelease(controls.elbow_flex.negative)
|
587 |
+
}
|
588 |
+
disabled={!teleopState?.isActive}
|
589 |
/>
|
590 |
<VirtualKey
|
591 |
label="D"
|
592 |
subLabel="Wrist-"
|
593 |
+
isPressed={
|
594 |
+
!!keyStates[controls.wrist_flex.negative]?.pressed
|
595 |
+
}
|
596 |
+
onMouseDown={() =>
|
597 |
+
simulateKeyPress(controls.wrist_flex.negative)
|
598 |
+
}
|
599 |
+
onMouseUp={() =>
|
600 |
+
simulateKeyRelease(controls.wrist_flex.negative)
|
601 |
+
}
|
602 |
+
disabled={!teleopState?.isActive}
|
603 |
/>
|
604 |
</div>
|
605 |
<span className="font-bold text-sm font-sans">Elbow/Wrist</span>
|
|
|
609 |
<VirtualKey
|
610 |
label="Q"
|
611 |
subLabel="Roll+"
|
612 |
+
isPressed={
|
613 |
+
!!keyStates[controls.wrist_roll.positive]?.pressed
|
614 |
+
}
|
615 |
+
onMouseDown={() =>
|
616 |
+
simulateKeyPress(controls.wrist_roll.positive)
|
617 |
+
}
|
618 |
+
onMouseUp={() =>
|
619 |
+
simulateKeyRelease(controls.wrist_roll.positive)
|
620 |
+
}
|
621 |
+
disabled={!teleopState?.isActive}
|
622 |
/>
|
623 |
<VirtualKey
|
624 |
label="E"
|
625 |
subLabel="Roll-"
|
626 |
+
isPressed={
|
627 |
+
!!keyStates[controls.wrist_roll.negative]?.pressed
|
628 |
+
}
|
629 |
+
onMouseDown={() =>
|
630 |
+
simulateKeyPress(controls.wrist_roll.negative)
|
631 |
+
}
|
632 |
+
onMouseUp={() =>
|
633 |
+
simulateKeyRelease(controls.wrist_roll.negative)
|
634 |
+
}
|
635 |
+
disabled={!teleopState?.isActive}
|
636 |
/>
|
637 |
</div>
|
638 |
<div className="flex gap-2">
|
639 |
+
<VirtualKey
|
640 |
+
label="O"
|
641 |
+
subLabel="Grip+"
|
642 |
+
isPressed={!!keyStates[controls.gripper.positive]?.pressed}
|
643 |
+
onMouseDown={() =>
|
644 |
+
simulateKeyPress(controls.gripper.positive)
|
645 |
+
}
|
646 |
+
onMouseUp={() =>
|
647 |
+
simulateKeyRelease(controls.gripper.positive)
|
648 |
+
}
|
649 |
+
disabled={!teleopState?.isActive}
|
650 |
+
/>
|
651 |
+
<VirtualKey
|
652 |
+
label="C"
|
653 |
+
subLabel="Grip-"
|
654 |
+
isPressed={!!keyStates[controls.gripper.negative]?.pressed}
|
655 |
+
onMouseDown={() =>
|
656 |
+
simulateKeyPress(controls.gripper.negative)
|
657 |
+
}
|
658 |
+
onMouseUp={() =>
|
659 |
+
simulateKeyRelease(controls.gripper.negative)
|
660 |
+
}
|
661 |
+
disabled={!teleopState?.isActive}
|
662 |
+
/>
|
663 |
</div>
|
664 |
<span className="font-bold text-sm font-sans">Roll/Grip</span>
|
665 |
</div>
|
|
|
668 |
<div className="flex justify-between items-center font-mono text-sm">
|
669 |
<div className="flex items-center gap-2 text-muted-foreground">
|
670 |
<Keyboard className="w-4 h-4" />
|
671 |
+
<span>
|
672 |
+
Active Keys:{" "}
|
673 |
+
{Object.values(keyStates).filter((k) => k.pressed).length}
|
674 |
+
</span>
|
675 |
</div>
|
676 |
<TooltipProvider>
|
677 |
<Tooltip>
|
|
|
679 |
<div
|
680 |
className={cn(
|
681 |
"w-10 h-6 border rounded-md flex items-center justify-center font-mono text-xs transition-all",
|
682 |
+
"select-none user-select-none",
|
683 |
+
!teleopState?.isActive &&
|
684 |
+
"opacity-50 cursor-not-allowed",
|
685 |
+
teleopState?.isActive &&
|
686 |
+
"cursor-pointer hover:bg-white/5",
|
687 |
+
keyStates[controls.stop]?.pressed
|
688 |
? "bg-destructive text-destructive-foreground border-destructive"
|
689 |
+
: "bg-background"
|
690 |
)}
|
691 |
+
onMouseDown={(e) => {
|
692 |
+
e.preventDefault();
|
693 |
+
if (teleopState?.isActive) {
|
694 |
+
simulateKeyPress(controls.stop);
|
695 |
+
}
|
696 |
+
}}
|
697 |
+
onMouseUp={(e) => {
|
698 |
+
e.preventDefault();
|
699 |
+
if (teleopState?.isActive) {
|
700 |
+
simulateKeyRelease(controls.stop);
|
701 |
+
}
|
702 |
+
}}
|
703 |
+
onMouseLeave={(e) => {
|
704 |
+
e.preventDefault();
|
705 |
+
if (teleopState?.isActive) {
|
706 |
+
simulateKeyRelease(controls.stop);
|
707 |
+
}
|
708 |
+
}}
|
709 |
>
|
710 |
ESC
|
711 |
</div>
|
|
|
719 |
</div>
|
720 |
</div>
|
721 |
</Card>
|
722 |
+
);
|
723 |
}
|
examples/cyberpunk-standalone/src/global.css
CHANGED
@@ -123,35 +123,11 @@
|
|
123 |
opacity: 1;
|
124 |
}
|
125 |
body.dark {
|
126 |
-
background-image:
|
127 |
-
circle at var(--mouse-x) var(--mouse-y),
|
128 |
-
rgba(255, 193, 7, 0.08),
|
129 |
-
transparent 20vw
|
130 |
-
),
|
131 |
-
url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(255 255 255 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e");
|
132 |
}
|
133 |
}
|
134 |
|
135 |
@layer utilities {
|
136 |
-
.scanline-overlay::after {
|
137 |
-
content: "";
|
138 |
-
position: absolute;
|
139 |
-
top: 0;
|
140 |
-
left: 0;
|
141 |
-
right: 0;
|
142 |
-
bottom: 0;
|
143 |
-
pointer-events: none;
|
144 |
-
background: linear-gradient(
|
145 |
-
to bottom,
|
146 |
-
rgba(18, 18, 18, 0) 50%,
|
147 |
-
rgba(0, 0, 0, 0.2) 70%,
|
148 |
-
rgba(18, 18, 18, 0.75)
|
149 |
-
);
|
150 |
-
animation: scanline 8s linear infinite;
|
151 |
-
opacity: 0.1;
|
152 |
-
height: 200%;
|
153 |
-
}
|
154 |
-
|
155 |
.text-glitch {
|
156 |
animation: text-glitch 0.3s linear infinite alternate-reverse;
|
157 |
}
|
|
|
123 |
opacity: 1;
|
124 |
}
|
125 |
body.dark {
|
126 |
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(255 255 255 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e");
|
|
|
|
|
|
|
|
|
|
|
127 |
}
|
128 |
}
|
129 |
|
130 |
@layer utilities {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
.text-glitch {
|
132 |
animation: text-glitch 0.3s linear infinite alternate-reverse;
|
133 |
}
|
examples/cyberpunk-standalone/src/lib/mock-api.ts
DELETED
@@ -1,137 +0,0 @@
|
|
1 |
-
import type { RobotConnection, LiveCalibrationData, WebCalibrationResults, TeleoperationState } from "@/types/robot"
|
2 |
-
|
3 |
-
const MOCK_MOTOR_NAMES = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"]
|
4 |
-
|
5 |
-
export const lerobot = {
|
6 |
-
isWebSerialSupported: () => true,
|
7 |
-
findPort: async ({
|
8 |
-
robotConfigs,
|
9 |
-
onMessage,
|
10 |
-
}: { robotConfigs?: any[]; onMessage?: (msg: string) => void }): Promise<{ result: Promise<RobotConnection[]> }> => {
|
11 |
-
onMessage?.("Searching for robots...")
|
12 |
-
const resultPromise = new Promise<RobotConnection[]>((resolve) => {
|
13 |
-
setTimeout(() => {
|
14 |
-
if (robotConfigs && robotConfigs.length > 0) {
|
15 |
-
onMessage?.(`Found ${robotConfigs.length} saved robots.`)
|
16 |
-
const reconnectedRobots = robotConfigs.map((config) => ({
|
17 |
-
...config,
|
18 |
-
isConnected: Math.random() > 0.3,
|
19 |
-
port: {},
|
20 |
-
}))
|
21 |
-
resolve(reconnectedRobots)
|
22 |
-
} else {
|
23 |
-
onMessage?.("Simulating successful device connection.")
|
24 |
-
const newRobot: RobotConnection = {
|
25 |
-
port: {},
|
26 |
-
name: "Cyber-Arm 7",
|
27 |
-
isConnected: true,
|
28 |
-
robotType: "so100_follower",
|
29 |
-
robotId: `robot-${Date.now()}`,
|
30 |
-
serialNumber: `SN-${Math.floor(Math.random() * 1000000)}`,
|
31 |
-
}
|
32 |
-
resolve([newRobot])
|
33 |
-
}
|
34 |
-
}, 1500)
|
35 |
-
})
|
36 |
-
return { result: resultPromise }
|
37 |
-
},
|
38 |
-
calibrate: async (
|
39 |
-
robot: RobotConnection,
|
40 |
-
{
|
41 |
-
onLiveUpdate,
|
42 |
-
onProgress,
|
43 |
-
}: { onLiveUpdate: (data: LiveCalibrationData) => void; onProgress: (msg: string) => void },
|
44 |
-
): Promise<{ result: Promise<WebCalibrationResults>; stop: () => void }> => {
|
45 |
-
onProgress("MOCK: Starting calibration... Move all joints to their limits.")
|
46 |
-
let intervalId: NodeJS.Timeout
|
47 |
-
const liveData: LiveCalibrationData = MOCK_MOTOR_NAMES.reduce((acc, name) => {
|
48 |
-
acc[name] = { current: 2048, min: 4095, max: 0, range: 0 }
|
49 |
-
return acc
|
50 |
-
}, {} as LiveCalibrationData)
|
51 |
-
intervalId = setInterval(() => {
|
52 |
-
MOCK_MOTOR_NAMES.forEach((name) => {
|
53 |
-
const motor = liveData[name]
|
54 |
-
motor.current = 2048 + Math.floor((Math.random() - 0.5) * 4000)
|
55 |
-
motor.min = Math.min(motor.min, motor.current)
|
56 |
-
motor.max = Math.max(motor.max, motor.current)
|
57 |
-
motor.range = motor.max - motor.min
|
58 |
-
})
|
59 |
-
onLiveUpdate({ ...liveData })
|
60 |
-
}, 100)
|
61 |
-
const resultPromise = new Promise<WebCalibrationResults>((resolve) => {
|
62 |
-
;(stop as any)._resolver = resolve
|
63 |
-
})
|
64 |
-
const stop = () => {
|
65 |
-
clearInterval(intervalId)
|
66 |
-
onProgress("MOCK: Calibration recording finished.")
|
67 |
-
const finalResults = MOCK_MOTOR_NAMES.reduce((acc, name) => {
|
68 |
-
acc[name] = { min: liveData[name].min, max: liveData[name].max }
|
69 |
-
return acc
|
70 |
-
}, {} as WebCalibrationResults)
|
71 |
-
;(stop as any)._resolver(finalResults)
|
72 |
-
}
|
73 |
-
return { result: resultPromise, stop }
|
74 |
-
},
|
75 |
-
teleoperate: async (
|
76 |
-
robot: RobotConnection,
|
77 |
-
{
|
78 |
-
calibrationData,
|
79 |
-
onStateUpdate,
|
80 |
-
}: { calibrationData: WebCalibrationResults; onStateUpdate: (state: TeleoperationState) => void },
|
81 |
-
): Promise<{
|
82 |
-
start: () => void
|
83 |
-
stop: () => void
|
84 |
-
updateKeyState: (key: string, pressed: boolean) => void
|
85 |
-
moveMotor: (motorName: string, position: number) => void
|
86 |
-
}> => {
|
87 |
-
let isActive = false
|
88 |
-
let intervalId: NodeJS.Timeout
|
89 |
-
const keyStates: { [key: string]: { pressed: boolean } } = {}
|
90 |
-
const motorConfigs = MOCK_MOTOR_NAMES.map((name) => ({
|
91 |
-
name,
|
92 |
-
currentPosition: calibrationData[name] ? (calibrationData[name].min + calibrationData[name].max) / 2 : 2048,
|
93 |
-
minPosition: calibrationData[name]?.min ?? 0,
|
94 |
-
maxPosition: calibrationData[name]?.max ?? 4095,
|
95 |
-
}))
|
96 |
-
const teleopState: TeleoperationState = { isActive, motorConfigs, keyStates }
|
97 |
-
const updateLoop = () => onStateUpdate({ ...teleopState, motorConfigs: [...motorConfigs] })
|
98 |
-
return {
|
99 |
-
start: () => {
|
100 |
-
if (isActive) return
|
101 |
-
isActive = true
|
102 |
-
teleopState.isActive = true
|
103 |
-
intervalId = setInterval(updateLoop, 100)
|
104 |
-
onStateUpdate({ ...teleopState })
|
105 |
-
},
|
106 |
-
stop: () => {
|
107 |
-
if (!isActive) return
|
108 |
-
isActive = false
|
109 |
-
teleopState.isActive = false
|
110 |
-
clearInterval(intervalId)
|
111 |
-
onStateUpdate({ ...teleopState })
|
112 |
-
},
|
113 |
-
updateKeyState: (key: string, pressed: boolean) => {
|
114 |
-
keyStates[key] = { pressed }
|
115 |
-
teleopState.keyStates = { ...keyStates }
|
116 |
-
onStateUpdate({ ...teleopState })
|
117 |
-
},
|
118 |
-
moveMotor: (motorName: string, position: number) => {
|
119 |
-
const motor = motorConfigs.find((m) => m.name === motorName)
|
120 |
-
if (motor) {
|
121 |
-
motor.currentPosition = position
|
122 |
-
onStateUpdate({ ...teleopState, motorConfigs: [...motorConfigs] })
|
123 |
-
}
|
124 |
-
},
|
125 |
-
}
|
126 |
-
},
|
127 |
-
SO100_KEYBOARD_CONTROLS: {
|
128 |
-
shoulder_pan: { positive: "ArrowRight", negative: "ArrowLeft" },
|
129 |
-
shoulder_lift: { positive: "ArrowUp", negative: "ArrowDown" },
|
130 |
-
elbow_flex: { positive: "w", negative: "s" },
|
131 |
-
wrist_flex: { positive: "a", negative: "d" },
|
132 |
-
wrist_roll: { positive: "q", negative: "e" },
|
133 |
-
gripper: { positive: "o", negative: "c" },
|
134 |
-
stop: "Escape",
|
135 |
-
},
|
136 |
-
MOCK_MOTOR_NAMES,
|
137 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/cyberpunk-standalone/src/lib/unified-storage.ts
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Unified storage for robot device data and calibration
|
3 |
+
* Manages device persistence using localStorage with serial numbers as keys
|
4 |
+
*/
|
5 |
+
|
6 |
+
import type { WebCalibrationResults } from "@lerobot/web";
|
7 |
+
|
8 |
+
export interface DeviceInfo {
|
9 |
+
serialNumber: string;
|
10 |
+
robotType: string;
|
11 |
+
robotId: string;
|
12 |
+
usbMetadata?: {
|
13 |
+
vendorId?: number;
|
14 |
+
productId?: number;
|
15 |
+
serialNumber?: string;
|
16 |
+
manufacturer?: string;
|
17 |
+
product?: string;
|
18 |
+
};
|
19 |
+
}
|
20 |
+
|
21 |
+
export interface CalibrationMetadata {
|
22 |
+
timestamp: string;
|
23 |
+
readCount: number;
|
24 |
+
}
|
25 |
+
|
26 |
+
export interface UnifiedRobotData {
|
27 |
+
device_info: DeviceInfo;
|
28 |
+
calibration?: WebCalibrationResults & {
|
29 |
+
device_type?: string;
|
30 |
+
device_id?: string;
|
31 |
+
calibrated_at?: string;
|
32 |
+
platform?: string;
|
33 |
+
api?: string;
|
34 |
+
};
|
35 |
+
calibration_metadata?: CalibrationMetadata;
|
36 |
+
}
|
37 |
+
|
38 |
+
export function getUnifiedRobotData(
|
39 |
+
serialNumber: string
|
40 |
+
): UnifiedRobotData | null {
|
41 |
+
try {
|
42 |
+
const key = `lerobotjs-${serialNumber}`;
|
43 |
+
const data = localStorage.getItem(key);
|
44 |
+
if (!data) return null;
|
45 |
+
|
46 |
+
const parsed = JSON.parse(data);
|
47 |
+
return parsed as UnifiedRobotData;
|
48 |
+
} catch (error) {
|
49 |
+
console.warn(`Failed to load robot data for ${serialNumber}:`, error);
|
50 |
+
return null;
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
export function saveUnifiedRobotData(
|
55 |
+
serialNumber: string,
|
56 |
+
data: UnifiedRobotData
|
57 |
+
): void {
|
58 |
+
try {
|
59 |
+
const key = `lerobotjs-${serialNumber}`;
|
60 |
+
localStorage.setItem(key, JSON.stringify(data));
|
61 |
+
} catch (error) {
|
62 |
+
console.warn(`Failed to save robot data for ${serialNumber}:`, error);
|
63 |
+
}
|
64 |
+
}
|
65 |
+
|
66 |
+
export function saveCalibrationData(
|
67 |
+
serialNumber: string,
|
68 |
+
calibrationData: WebCalibrationResults,
|
69 |
+
metadata: CalibrationMetadata
|
70 |
+
): void {
|
71 |
+
try {
|
72 |
+
const existingData = getUnifiedRobotData(serialNumber);
|
73 |
+
if (!existingData) {
|
74 |
+
console.warn(
|
75 |
+
`No device info found for ${serialNumber}, cannot save calibration`
|
76 |
+
);
|
77 |
+
return;
|
78 |
+
}
|
79 |
+
|
80 |
+
const updatedData: UnifiedRobotData = {
|
81 |
+
...existingData,
|
82 |
+
calibration: calibrationData,
|
83 |
+
calibration_metadata: metadata,
|
84 |
+
};
|
85 |
+
|
86 |
+
saveUnifiedRobotData(serialNumber, updatedData);
|
87 |
+
} catch (error) {
|
88 |
+
console.warn(`Failed to save calibration data for ${serialNumber}:`, error);
|
89 |
+
}
|
90 |
+
}
|
91 |
+
|
92 |
+
export function saveDeviceInfo(
|
93 |
+
serialNumber: string,
|
94 |
+
deviceInfo: DeviceInfo
|
95 |
+
): void {
|
96 |
+
try {
|
97 |
+
const existingData = getUnifiedRobotData(serialNumber);
|
98 |
+
const updatedData: UnifiedRobotData = {
|
99 |
+
...existingData,
|
100 |
+
device_info: deviceInfo,
|
101 |
+
};
|
102 |
+
|
103 |
+
saveUnifiedRobotData(serialNumber, updatedData);
|
104 |
+
} catch (error) {
|
105 |
+
console.warn(`Failed to save device info for ${serialNumber}:`, error);
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
export function getAllSavedRobots(): DeviceInfo[] {
|
110 |
+
try {
|
111 |
+
const robots: DeviceInfo[] = [];
|
112 |
+
|
113 |
+
for (let i = 0; i < localStorage.length; i++) {
|
114 |
+
const key = localStorage.key(i);
|
115 |
+
if (key?.startsWith("lerobotjs-")) {
|
116 |
+
const data = getUnifiedRobotData(key.replace("lerobotjs-", ""));
|
117 |
+
if (data?.device_info) {
|
118 |
+
robots.push(data.device_info);
|
119 |
+
}
|
120 |
+
}
|
121 |
+
}
|
122 |
+
|
123 |
+
return robots;
|
124 |
+
} catch (error) {
|
125 |
+
console.warn("Failed to load saved robots:", error);
|
126 |
+
return [];
|
127 |
+
}
|
128 |
+
}
|
129 |
+
|
130 |
+
export function removeRobotData(serialNumber: string): void {
|
131 |
+
try {
|
132 |
+
const key = `lerobotjs-${serialNumber}`;
|
133 |
+
localStorage.removeItem(key);
|
134 |
+
} catch (error) {
|
135 |
+
console.warn(`Failed to remove robot data for ${serialNumber}:`, error);
|
136 |
+
}
|
137 |
+
}
|
examples/cyberpunk-standalone/src/types/robot.ts
CHANGED
@@ -1,35 +1,6 @@
|
|
1 |
-
export
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
serialNumber?: string
|
8 |
-
}
|
9 |
-
|
10 |
-
export interface LiveCalibrationData {
|
11 |
-
[motorName: string]: {
|
12 |
-
current: number
|
13 |
-
min: number
|
14 |
-
max: number
|
15 |
-
range: number
|
16 |
-
}
|
17 |
-
}
|
18 |
-
|
19 |
-
export interface WebCalibrationResults {
|
20 |
-
[motorName: string]: {
|
21 |
-
min: number
|
22 |
-
max: number
|
23 |
-
}
|
24 |
-
}
|
25 |
-
|
26 |
-
export interface TeleoperationState {
|
27 |
-
isActive: boolean
|
28 |
-
motorConfigs: Array<{
|
29 |
-
name: string
|
30 |
-
currentPosition: number
|
31 |
-
minPosition: number
|
32 |
-
maxPosition: number
|
33 |
-
}>
|
34 |
-
keyStates: { [key: string]: { pressed: boolean } }
|
35 |
-
}
|
|
|
1 |
+
export type {
|
2 |
+
RobotConnection,
|
3 |
+
LiveCalibrationData,
|
4 |
+
WebCalibrationResults,
|
5 |
+
TeleoperationState,
|
6 |
+
} from "@lerobot/web";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/cyberpunk-standalone/tsconfig.tsbuildinfo
ADDED
The diff for this file is too large to render.
See raw diff
|
|