NERDDISCO commited on
Commit
7a0c9ff
·
1 Parent(s): a8d792f

feat: make the cyberpunk example work with @lerobot/web

Browse files
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 { lerobot } from "@/lib/mock-api";
14
- import { useLocalStorage } from "@/hooks/use-local-storage";
15
- import type { RobotConnection } from "@/types/robot";
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  function App() {
18
  const [view, setView] = useState<
19
  "dashboard" | "calibrating" | "teleoperating"
20
  >("dashboard");
21
- const [robots, setRobots] = useLocalStorage<RobotConnection[]>(
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
- const robotConfigs = robots.map(({ port, ...config }) => config);
38
- if (robotConfigs.length > 0) {
39
- const findPortProcess = await lerobot.findPort({
40
- robotConfigs,
41
- onMessage: (msg) => setLogs((prev) => [...prev, msg]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  });
43
- const reconnectedRobots = await findPortProcess.result;
44
- setRobots(reconnectedRobots);
45
  }
46
- setIsConnecting(false);
47
  };
 
48
  loadSavedRobots();
49
- }, []);
50
 
51
  const handleFindNewRobots = async () => {
52
- setIsConnecting(true);
53
- const findPortProcess = await lerobot.findPort({
54
- onMessage: (msg) => setLogs((prev) => [...prev, msg]),
55
- });
56
- const newRobots = await findPortProcess.result;
57
- setRobots((prev) => {
58
- const existingIds = new Set(prev.map((r) => r.robotId));
59
- const uniqueNewRobots = newRobots.filter(
60
- (r) => !existingIds.has(r.robotId)
61
- );
62
- return [...prev, ...uniqueNewRobots];
63
- });
64
- if (newRobots.length > 0) {
65
- setEditingRobot(newRobots[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
- setIsConnecting(false);
68
  };
69
 
70
  const handleUpdateRobot = (updatedRobot: RobotConnection) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  setRobots((prev) =>
72
- prev.map((r) => (r.robotId === updatedRobot.robotId ? updatedRobot : 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.name}
161
  >
162
- {selectedRobot.name.toUpperCase()}
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.name}
173
  >
174
- {selectedRobot.name.toUpperCase()}
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 scanline-overlay">
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 = ({ label, subLabel, isPressed }: VirtualKeyProps) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 && <span className="text-xs text-muted-foreground mt-1 font-mono">{subLabel}</span>}
 
 
 
 
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 { lerobot } from "@/lib/mock-api"
7
- import { useLocalStorage } from "@/hooks/use-local-storage"
8
- import { MotorCalibrationVisual } from "@/components/motor-calibration-visual"
9
- import type { RobotConnection, LiveCalibrationData, WebCalibrationResults } from "@/types/robot"
 
 
 
 
 
 
 
 
 
 
 
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 [calibrationProcess, setCalibrationProcess] = useState<{
20
- stop: () => void
21
- result: Promise<WebCalibrationResults>
22
- } | null>(null)
23
- const [calibrationResults, setCalibrationResults] = useLocalStorage<WebCalibrationResults | null>(
24
- `calibration-${robot.robotId}`,
25
- null,
26
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  const handleStart = async () => {
29
- setIsCalibrating(true)
30
- const process = await lerobot.calibrate(robot, {
31
- onLiveUpdate: setLiveData,
32
- onProgress: setStatus,
33
- })
34
- setCalibrationProcess(process)
35
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  const handleFinish = async () => {
38
  if (calibrationProcess) {
39
- calibrationProcess.stop()
40
- const results = await calibrationProcess.result
41
- setCalibrationResults(results)
42
- setIsCalibrating(false)
43
- setCalibrationProcess(null)
 
 
 
 
 
 
 
 
 
44
  }
45
- }
46
 
47
  const downloadJson = () => {
48
- if (!calibrationResults) return
49
- const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(calibrationResults, null, 2))
50
- const downloadAnchorNode = document.createElement("a")
51
- downloadAnchorNode.setAttribute("href", dataStr)
52
- downloadAnchorNode.setAttribute("download", `${robot.robotId}_calibration.json`)
53
- document.body.appendChild(downloadAnchorNode)
54
- downloadAnchorNode.click()
55
- downloadAnchorNode.remove()
56
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  const motorData = useMemo(
59
  () =>
60
  liveData
61
  ? Object.entries(liveData)
62
- : lerobot.MOCK_MOTOR_NAMES.map((name) => [name, { current: 0, min: 4095, max: 0, range: 0 }]),
63
- [liveData],
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">motor calibration</h3>
73
- <p className="text-sm text-muted-foreground font-mono">move all joints to their limits</p>
 
 
74
  </div>
75
  </div>
76
  <div className="flex gap-4">
77
  {!isCalibrating ? (
78
- <Button onClick={handleStart} size="lg">
79
- Start Calibration
 
 
 
 
80
  </Button>
81
  ) : (
82
  <Button onClick={handleFinish} variant="destructive" size="lg">
83
  Finish Recording
84
  </Button>
85
  )}
86
- <Button onClick={downloadJson} variant="outline" size="lg" disabled={!calibrationResults}>
 
 
 
 
 
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 key={name} name={name} data={data} />
 
 
 
 
 
 
 
 
 
 
 
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 { Settings, Gamepad2, Trash2, Pencil, Plus, ExternalLink } from "lucide-react"
4
- import { Button } from "@/components/ui/button"
5
- import { Card, CardFooter } from "@/components/ui/card"
6
- import { Badge } from "@/components/ui/badge"
7
- import { cn } from "@/lib/utils"
8
- import HudCorners from "@/components/hud-corners"
9
- import type { RobotConnection } from "@/types/robot"
 
 
 
 
 
 
 
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">device registry</h3>
 
 
39
  <div className="flex items-center gap-2 mt-1">
40
- <span className="text-xs text-muted-foreground font-mono">currently supports SO-100 </span>
 
 
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 onClick={onFindNew} disabled={isConnecting} size="lg" className="font-mono uppercase">
 
 
 
 
 
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">scanning for units</h4>
71
- <p className="text-sm text-muted-foreground mb-8">searching for available devices...</p>
 
 
 
 
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">no units detected</h4>
 
 
79
 
80
- <Button onClick={onFindNew} size="lg" className="font-mono uppercase">
 
 
 
 
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">{robot.name}</h4>
 
 
99
  <div className="flex items-center gap-2 mt-1">
100
- <span className="text-xs text-muted-foreground font-mono">{robot.serialNumber}</span>
101
- <span className="text-xs text-muted-foreground">•</span>
102
- <span className="text-xs font-mono uppercase text-muted-foreground">{robot.robotType}</span>
 
 
 
 
 
 
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">status:</span>
 
 
121
  <div className="text-primary uppercase">
122
  {robot.isConnected ? "operational" : "disconnected"}
123
  </div>
124
  </div>
125
  <div>
126
- <span className="text-muted-foreground uppercase">type:</span>
127
- <div className="text-accent uppercase">{robot.robotType}</div>
 
 
 
 
128
  </div>
129
  </div>
130
  </div>
@@ -157,7 +197,11 @@ export function DeviceDashboard({
157
  <Button
158
  variant="destructive"
159
  size="sm"
160
- onClick={() => onRemove(robot.robotId)}
 
 
 
 
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog"
5
- import { Input } from "@/components/ui/input"
6
- import { Label } from "@/components/ui/label"
7
- import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
8
- import type { RobotConnection } from "@/types/robot"
 
 
 
 
 
 
 
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({ robot, isOpen, onOpenChange, onSave }: EditRobotDialogProps) {
18
- const [name, setName] = useState("")
19
- const [type, setType] = useState<"so100_follower" | "so100_leader">("so100_follower")
 
 
 
 
 
 
 
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 id="name" value={name} onChange={(e) => setName(e.target.value)} className="col-span-3 font-mono" />
 
 
 
 
 
48
  </div>
49
  <div className="grid grid-cols-4 items-center gap-4">
50
  <Label className="text-right">Type</Label>
51
- <RadioGroup value={type} onValueChange={(value) => setType(value as any)} className="col-span-3">
 
 
 
 
 
 
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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
9
- import { cn } from "@/lib/utils"
10
- import { lerobot } from "@/lib/mock-api"
11
- import { useLocalStorage } from "@/hooks/use-local-storage"
12
- import VirtualKey from "@/components/VirtualKey"
13
- import type { RobotConnection, WebCalibrationResults, TeleoperationState } from "@/types/robot"
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  interface TeleoperationViewProps {
16
- robot: RobotConnection
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  export function TeleoperationView({ robot }: TeleoperationViewProps) {
20
- const [localCalibrationData] = useLocalStorage<WebCalibrationResults | null>(`calibration-${robot.robotId}`, null)
21
- const [teleopState, setTeleopState] = useState<TeleoperationState | null>(null)
22
- const [teleopProcess, setTeleopProcess] = useState<{
23
- start: () => void
24
- stop: () => void
25
- updateKeyState: (key: string, pressed: boolean) => void
26
- moveMotor: (motorName: string, position: number) => void
27
- } | null>(null)
 
 
 
 
 
 
 
28
 
 
29
  const calibrationData = useMemo(() => {
30
- if (localCalibrationData) return localCalibrationData
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
- useEffect(() => {
38
- let process: Awaited<ReturnType<typeof lerobot.teleoperate>>
39
- const setup = async () => {
40
- process = await lerobot.teleoperate(robot, {
41
- calibrationData,
42
- onStateUpdate: setTeleopState,
43
- })
44
- setTeleopProcess(process)
45
  }
46
- setup()
47
 
48
- const handleKeyDown = (e: KeyboardEvent) => process?.updateKeyState(e.key, true)
49
- const handleKeyUp = (e: KeyboardEvent) => process?.updateKeyState(e.key, false)
 
50
 
51
- window.addEventListener("keydown", handleKeyDown)
52
- window.addEventListener("keyup", handleKeyUp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return () => {
55
- process?.stop()
56
- window.removeEventListener("keydown", handleKeyDown)
57
- window.removeEventListener("keyup", handleKeyUp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
- }, [robot, calibrationData])
60
-
61
- const motorConfigs =
62
- teleopState?.motorConfigs ??
63
- lerobot.MOCK_MOTOR_NAMES.map((name) => ({
64
- name,
65
- currentPosition: 2048,
66
- minPosition: calibrationData[name]?.min ?? 0,
67
- maxPosition: calibrationData[name]?.max ?? 4095,
68
- }))
69
- const keyStates = teleopState?.keyStates ?? {}
70
- const controls = lerobot.SO100_KEYBOARD_CONTROLS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">robot control</h3>
 
 
80
  <p className="text-sm text-muted-foreground font-mono">
81
- manual <span className="text-muted-foreground">teleoperate</span> interface
 
 
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={() => teleopProcess?.stop()} variant="destructive" size="lg">
89
  <PowerOff className="w-5 h-5 mr-2" /> Stop Control
90
  </Button>
91
  ) : (
92
- <Button onClick={() => teleopProcess?.start()} size="lg">
 
 
 
 
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">status:</span>
 
 
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">Motor Control</h3>
 
 
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">{motor.name}</label>
 
 
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) => teleopProcess?.moveMotor(motor.name, val[0])}
126
  disabled={!teleopState?.isActive}
 
127
  />
128
- <span className="text-lg font-mono w-16 text-right text-accent">
 
 
 
 
 
 
 
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">Keyboard Layout & Status</h3>
 
 
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={!!keyStates[controls.shoulder_lift.positive]?.pressed}
 
 
 
 
 
 
 
 
 
145
  />
146
  <div className="flex gap-2">
147
  <VirtualKey
148
  label="←"
149
  subLabel="Pan-"
150
- isPressed={!!keyStates[controls.shoulder_pan.negative]?.pressed}
 
 
 
 
 
 
 
 
 
151
  />
152
  <VirtualKey
153
  label="↓"
154
  subLabel="Lift-"
155
- isPressed={!!keyStates[controls.shoulder_lift.negative]?.pressed}
 
 
 
 
 
 
 
 
 
156
  />
157
  <VirtualKey
158
  label="→"
159
  subLabel="Pan+"
160
- isPressed={!!keyStates[controls.shoulder_pan.positive]?.pressed}
 
 
 
 
 
 
 
 
 
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={!!keyStates[controls.wrist_flex.positive]?.pressed}
 
 
 
 
 
 
 
 
 
176
  />
177
  <VirtualKey
178
  label="S"
179
  subLabel="Elbow-"
180
- isPressed={!!keyStates[controls.elbow_flex.negative]?.pressed}
 
 
 
 
 
 
 
 
 
181
  />
182
  <VirtualKey
183
  label="D"
184
  subLabel="Wrist-"
185
- isPressed={!!keyStates[controls.wrist_flex.negative]?.pressed}
 
 
 
 
 
 
 
 
 
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={!!keyStates[controls.wrist_roll.positive]?.pressed}
 
 
 
 
 
 
 
 
 
196
  />
197
  <VirtualKey
198
  label="E"
199
  subLabel="Roll-"
200
- isPressed={!!keyStates[controls.wrist_roll.negative]?.pressed}
 
 
 
 
 
 
 
 
 
201
  />
202
  </div>
203
  <div className="flex gap-2">
204
- <VirtualKey label="O" subLabel="Grip+" isPressed={!!keyStates[controls.gripper.positive]?.pressed} />
205
- <VirtualKey label="C" subLabel="Grip-" isPressed={!!keyStates[controls.gripper.negative]?.pressed} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Active Keys: {Object.values(keyStates).filter((k) => k.pressed).length}</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
- !!keyStates[controls.stop]?.pressed
 
 
 
 
 
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: radial-gradient(
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 interface RobotConnection {
2
- port: object
3
- name: string
4
- isConnected: boolean
5
- robotType?: "so100_follower" | "so100_leader"
6
- robotId: string
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