GitHub CI commited on
Commit
df3ef41
·
0 Parent(s):

Sync from leLab @ 39fcfee669f0b98e7a1fc4dde23025ea8cc7e5ce

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +40 -0
  2. .gitignore +48 -0
  3. Dockerfile +38 -0
  4. HTTPS_SETUP.md +219 -0
  5. LICENSE +202 -0
  6. README.md +20 -0
  7. __init__.py +0 -0
  8. components.json +20 -0
  9. eslint.config.js +29 -0
  10. index.html +19 -0
  11. package-lock.json +0 -0
  12. package.json +88 -0
  13. postcss.config.js +6 -0
  14. public/favicon.ico +0 -0
  15. public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png +0 -0
  16. public/placeholder.svg +1 -0
  17. public/robots.txt +14 -0
  18. public/so-101-urdf/CMakeLists.txt +37 -0
  19. public/so-101-urdf/README.md +45 -0
  20. public/so-101-urdf/config/joint_names_so_arm_urdf.yaml +1 -0
  21. public/so-101-urdf/joints_properties.xml +12 -0
  22. public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl +3 -0
  23. public/so-101-urdf/meshes/base_so101_v2.stl +3 -0
  24. public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl +3 -0
  25. public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl +3 -0
  26. public/so-101-urdf/meshes/moving_jaw_so101_v1.stl +3 -0
  27. public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl +3 -0
  28. public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl +3 -0
  29. public/so-101-urdf/meshes/sts3215_03a_v1.stl +3 -0
  30. public/so-101-urdf/meshes/under_arm_so101_v1.stl +3 -0
  31. public/so-101-urdf/meshes/upper_arm_so101_v1.stl +3 -0
  32. public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl +3 -0
  33. public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl +3 -0
  34. public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl +3 -0
  35. public/so-101-urdf/package.xml +26 -0
  36. public/so-101-urdf/urdf/so101_new_calib.urdf +435 -0
  37. src/.gitignore +1 -0
  38. src/App.tsx +65 -0
  39. src/components/Footer.tsx +68 -0
  40. src/components/Logo.tsx +19 -0
  41. src/components/SingleTabGuard.tsx +136 -0
  42. src/components/TeleopStopNotice.tsx +36 -0
  43. src/components/UpdateNotice.tsx +194 -0
  44. src/components/UrdfViewer.tsx +304 -0
  45. src/components/control/CameraFeed.tsx +46 -0
  46. src/components/control/CommandBar.tsx +81 -0
  47. src/components/control/MetricsPanel.tsx +190 -0
  48. src/components/control/TeleopCameraPanel.tsx +94 -0
  49. src/components/control/VisualizerPanel.tsx +52 -0
  50. src/components/jobs/CheckpointDropdown.tsx +58 -0
.gitattributes ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.stl filter=lfs diff=lfs merge=lfs -text
37
+ *.lockb filter=lfs diff=lfs merge=lfs -text
38
+
39
+ # Build output: store as plain binary (overrides LFS) so the wheel can ship real files
40
+ dist/** -filter -diff -merge
.gitignore ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ /node_modules
3
+ /.pnp
4
+ .pnp.js
5
+
6
+ # Testing
7
+ /coverage
8
+
9
+ # Production
10
+ /build
11
+
12
+ # Misc
13
+ .DS_Store
14
+ .env
15
+ .env.local
16
+ .env.development.local
17
+ .env.test.local
18
+ .env.production.local
19
+ .env*.local
20
+
21
+ # Debug logs
22
+ npm-debug.log*
23
+ yarn-debug.log*
24
+ yarn-error.log*
25
+
26
+ # IDE
27
+ .idea/
28
+ .vscode/
29
+ *.swp
30
+ *.swo
31
+
32
+ # TypeScript
33
+ *.tsbuildinfo
34
+
35
+ # Optional npm cache directory
36
+ .npm
37
+
38
+ # Optional eslint cache
39
+ .eslintcache
40
+
41
+ # Optional REPL history
42
+ .node_repl_history
43
+
44
+ # Output of 'npm pack'
45
+ *.tgz
46
+
47
+ # Yarn Integrity file
48
+ .yarn-integrity
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vite 8 requires Node ^20.19.0 or >=22.12.0.
2
+ FROM node:22-alpine
3
+
4
+ # Use the existing node user (usually UID 1000)
5
+ # Set up environment variables for the node user
6
+ ENV HOME=/home/node \
7
+ PATH=/home/node/.local/bin:$PATH
8
+
9
+ # Create and set up app directory owned by node user
10
+ # Go to user's home directory first to ensure it exists
11
+ WORKDIR $HOME
12
+ RUN mkdir -p $HOME/app && \
13
+ chown -R node:node $HOME/app && \
14
+ chmod -R 755 $HOME/app # Set initial permissions
15
+ WORKDIR $HOME/app
16
+
17
+ # Switch to the node user
18
+ USER node
19
+
20
+ # Copy package files (owned by node)
21
+ COPY --chown=node:node package*.json ./
22
+
23
+ # Install dependencies straight from the committed lockfile. `npm ci` honours
24
+ # the `resolved` URLs in package-lock.json and ignores the builder's configured
25
+ # registry, so the install can't be derailed by a slow/incomplete npm mirror.
26
+ RUN npm ci
27
+
28
+ # Copy the entire viewer directory (owned by node)
29
+ COPY --chown=node:node . .
30
+
31
+ # Build the application
32
+ RUN npm run build
33
+
34
+ # Expose port
35
+ EXPOSE 7860
36
+
37
+ # Start the application
38
+ CMD ["npm", "run", "preview", "--", "--port", "7860", "--host"]
HTTPS_SETUP.md ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HTTPS Setup for Local Development
2
+
3
+ This guide explains how to enable HTTPS for the frontend development server to ensure camera access works properly from mobile devices.
4
+
5
+ ## Why HTTPS is Required
6
+
7
+ Modern browsers require HTTPS for accessing sensitive APIs like `getUserMedia()` (camera access), especially when:
8
+
9
+ - Accessing from a different device (e.g., phone connecting to laptop's dev server)
10
+ - The origin is not `localhost` or `127.0.0.1`
11
+
12
+ Without HTTPS, camera access will be blocked by the browser's security policies.
13
+
14
+ ## Automatic Setup
15
+
16
+ We've already set up HTTPS for you! The certificates are generated and the development server is configured to use them.
17
+
18
+ ### Quick Start
19
+
20
+ 1. **Start the development server:**
21
+
22
+ ```bash
23
+ npm run dev
24
+ ```
25
+
26
+ 2. **Access from your computer:**
27
+
28
+ - `https://localhost:8080`
29
+ - `https://127.0.0.1:8080`
30
+
31
+ 3. **Access from your phone:**
32
+ - `https://192.168.1.103:8080` (or your current local IP)
33
+
34
+ ## Manual Setup (if needed)
35
+
36
+ If you need to regenerate certificates or set up on a new machine:
37
+
38
+ ```bash
39
+ # Run our setup script
40
+ ./scripts/setup-https.sh
41
+ ```
42
+
43
+ Or manually:
44
+
45
+ ```bash
46
+ # Install mkcert
47
+ brew install mkcert # macOS
48
+ # For other platforms: https://github.com/FiloSottile/mkcert#installation
49
+
50
+ # Install local CA
51
+ mkcert -install
52
+
53
+ # Generate certificates
54
+ mkdir -p certs
55
+ cd certs
56
+ mkcert localhost 127.0.0.1 $(ipconfig getifaddr en0) ::1
57
+ cd ..
58
+
59
+ # Start development server
60
+ npm run dev
61
+ ```
62
+
63
+ ## Certificate Details
64
+
65
+ - **Location:** `./certs/`
66
+ - **Files:**
67
+ - `localhost+3.pem` (certificate)
68
+ - `localhost+3-key.pem` (private key)
69
+ - **Validity:** 3 months
70
+ - **Domains:** localhost, 127.0.0.1, your local IP, IPv6 localhost
71
+
72
+ ## Mobile Device Setup
73
+
74
+ ### For Phone Access:
75
+
76
+ 1. **Ensure same WiFi network:** Your phone and development machine must be on the same WiFi network
77
+
78
+ 2. **Find your local IP:**
79
+
80
+ ```bash
81
+ ipconfig getifaddr en0 # macOS
82
+ # or check the console output when starting the dev server
83
+ ```
84
+
85
+ 3. **Access from phone:**
86
+
87
+ - Open Safari (iOS) or Chrome (Android)
88
+ - Navigate to `https://YOUR_LOCAL_IP:8080`
89
+ - You may see a security warning - this is normal for local certificates
90
+
91
+ 4. **Accept the certificate:**
92
+
93
+ - **iOS Safari:** Tap "Advanced" → "Proceed to website"
94
+ - **Android Chrome:** Tap "Advanced" → "Proceed to site (unsafe)"
95
+
96
+ 5. **Test camera access:**
97
+ - Go to the recording page OR use our camera test page: `https://YOUR_LOCAL_IP:8080/test-camera.html`
98
+ - Try to add a camera or click "Start Camera"
99
+ - The browser should prompt for camera permission
100
+ - Grant permission to test `getUserMedia()` functionality
101
+
102
+ ## Troubleshooting
103
+
104
+ ### Certificate Issues
105
+
106
+ If you see certificate errors:
107
+
108
+ ```bash
109
+ # Regenerate certificates
110
+ rm -rf certs/
111
+ ./scripts/setup-https.sh
112
+ ```
113
+
114
+ ### IP Address Changes
115
+
116
+ If your local IP changes (common with DHCP):
117
+
118
+ ```bash
119
+ # Check current IP
120
+ ipconfig getifaddr en0
121
+
122
+ # Regenerate certificates with new IP
123
+ rm -rf certs/
124
+ ./scripts/setup-https.sh
125
+ ```
126
+
127
+ ### Port Already in Use
128
+
129
+ If port 8080 is busy:
130
+
131
+ ```bash
132
+ # Check what's using the port
133
+ lsof -ti:8080
134
+
135
+ # Kill the process if needed
136
+ kill -9 $(lsof -ti:8080)
137
+ ```
138
+
139
+ ### Browser Cache Issues
140
+
141
+ If you're having issues after certificate changes:
142
+
143
+ 1. Clear browser cache and cookies for localhost
144
+ 2. Restart the browser
145
+ 3. Try incognito/private mode
146
+
147
+ ## Network Address Detection
148
+
149
+ The app automatically detects the appropriate URL for phone access:
150
+
151
+ - **Localhost access:** Automatically converts to network IP for QR codes
152
+ - **Network access:** Uses current URL as-is
153
+ - **Domain access:** Works with existing SSL certificates
154
+
155
+ You can see the detected address in:
156
+
157
+ - Browser console logs
158
+ - Camera configuration UI (blue info box)
159
+
160
+ ## Security Notes
161
+
162
+ - These certificates are only trusted on your local machine
163
+ - They're perfect for development but should never be used in production
164
+ - The private key is stored locally and should not be shared
165
+ - Certificates expire after 3 months for security
166
+
167
+ ## Production Deployment
168
+
169
+ For production:
170
+
171
+ - Use a proper SSL certificate from a trusted CA
172
+ - Configure your reverse proxy (nginx, Apache) or hosting platform
173
+ - Ensure HTTPS is enforced across your entire application
174
+
175
+ ## Verification
176
+
177
+ To verify HTTPS is working:
178
+
179
+ 1. **Check the URL bar:** Should show `https://` with a lock icon
180
+ 2. **Check console:** Network address detection should show HTTPS URLs
181
+ 3. **Test camera access:** `getUserMedia()` should work without security errors
182
+ 4. **Check from phone:** Camera permissions should be available
183
+
184
+ ### Quick Test Page
185
+
186
+ We've included a dedicated camera test page for easy verification:
187
+
188
+ **Desktop:** `https://localhost:8080/test-camera.html`
189
+ **Phone:** `https://192.168.1.103:8080/test-camera.html`
190
+
191
+ This page will:
192
+
193
+ - ✅ Verify HTTPS and secure context
194
+ - ✅ Test camera permissions and `getUserMedia()`
195
+ - ✅ Display real-time video stream
196
+ - ✅ Show detailed connection information
197
+ - ✅ Provide troubleshooting guidance
198
+
199
+ ### Expected Results
200
+
201
+ **✅ Success indicators:**
202
+
203
+ - Green protocol check: "Camera access should work!"
204
+ - Camera permission prompt appears
205
+ - Video stream displays without errors
206
+ - No console security warnings
207
+
208
+ **❌ Failure indicators:**
209
+
210
+ - Red protocol check: "Camera access may be blocked"
211
+ - `NotAllowedError` or `NotSecureError` in console
212
+ - No camera permission prompt
213
+ - "This site is not secure" warnings
214
+
215
+ ## Additional Resources
216
+
217
+ - [mkcert GitHub Repository](https://github.com/FiloSottile/mkcert)
218
+ - [MDN: getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
219
+ - [Chrome Developer: HTTPS for Localhost](https://web.dev/how-to-use-local-https/)
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: LeLab
3
+ emoji: ⚡
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ short_description: Simple Interface to use LeRobot
10
+ ---
11
+
12
+ <img width="1283" alt="Screenshot 2025-06-19 at 00 23 55" src="https://github.com/user-attachments/assets/ce1b5888-d24b-4adf-a422-694d4b0aa751" />
13
+
14
+ # LeLab official Space repository
15
+
16
+ If you've used [LeLab](https://huggingface.co/spaces/lerobot/LeLab) and want to contribute or found a bug, this is the place to be. This repo is directly hooked up to the Hugging Face space.
17
+
18
+ Here's the equivalent [backend](https://github.com/huggingface/leLab) that keeps the FastAPI server you run to actually wrap the LeRobot library.
19
+
20
+ We'll be updating this README shortly.
__init__.py ADDED
File without changes
components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/index.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": [
23
+ "warn",
24
+ { allowConstantExport: true },
25
+ ],
26
+ "@typescript-eslint/no-unused-vars": "off",
27
+ },
28
+ }
29
+ );
index.html ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>LeLab</title>
7
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
8
+ <meta name="description" content="LeRobot but on your browser." />
9
+
10
+ <meta property="og:title" content="LeLab" />
11
+ <meta property="og:description" content="LeRobot but on your browser." />
12
+ <meta property="og:type" content="website" />
13
+ </head>
14
+
15
+ <body>
16
+ <div id="root"></div>
17
+ <script type="module" src="/src/main.tsx"></script>
18
+ </body>
19
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vite_react_shadcn_ts",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "build:dev": "vite build --mode development",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "@hookform/resolvers": "^3.9.0",
15
+ "@radix-ui/react-accordion": "^1.2.0",
16
+ "@radix-ui/react-alert-dialog": "^1.1.1",
17
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
18
+ "@radix-ui/react-avatar": "^1.1.0",
19
+ "@radix-ui/react-checkbox": "^1.1.1",
20
+ "@radix-ui/react-collapsible": "^1.1.0",
21
+ "@radix-ui/react-context-menu": "^2.2.1",
22
+ "@radix-ui/react-dialog": "^1.1.2",
23
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
24
+ "@radix-ui/react-hover-card": "^1.1.1",
25
+ "@radix-ui/react-label": "^2.1.0",
26
+ "@radix-ui/react-menubar": "^1.1.1",
27
+ "@radix-ui/react-navigation-menu": "^1.2.0",
28
+ "@radix-ui/react-popover": "^1.1.1",
29
+ "@radix-ui/react-progress": "^1.1.0",
30
+ "@radix-ui/react-radio-group": "^1.2.0",
31
+ "@radix-ui/react-scroll-area": "^1.1.0",
32
+ "@radix-ui/react-select": "^2.1.1",
33
+ "@radix-ui/react-separator": "^1.1.0",
34
+ "@radix-ui/react-slider": "^1.2.0",
35
+ "@radix-ui/react-slot": "^1.1.0",
36
+ "@radix-ui/react-switch": "^1.1.0",
37
+ "@radix-ui/react-tabs": "^1.1.0",
38
+ "@radix-ui/react-toast": "^1.2.1",
39
+ "@radix-ui/react-toggle": "^1.1.0",
40
+ "@radix-ui/react-toggle-group": "^1.1.0",
41
+ "@radix-ui/react-tooltip": "^1.1.4",
42
+ "@react-three/drei": "^9.122.0",
43
+ "@react-three/fiber": "^8.18.0",
44
+ "@tanstack/react-query": "^5.56.2",
45
+ "class-variance-authority": "^0.7.1",
46
+ "clsx": "^2.1.1",
47
+ "cmdk": "^1.0.0",
48
+ "date-fns": "^3.6.0",
49
+ "embla-carousel-react": "^8.3.0",
50
+ "input-otp": "^1.2.4",
51
+ "jszip": "^3.10.1",
52
+ "lucide-react": "^0.462.0",
53
+ "next-themes": "^0.3.0",
54
+ "react": "^18.3.1",
55
+ "react-day-picker": "^8.10.1",
56
+ "react-dom": "^18.3.1",
57
+ "react-hook-form": "^7.53.0",
58
+ "react-resizable-panels": "^2.1.3",
59
+ "react-router-dom": "^7.0.0",
60
+ "recharts": "^2.12.7",
61
+ "sonner": "^1.5.0",
62
+ "tailwind-merge": "^2.5.2",
63
+ "tailwindcss-animate": "^1.0.7",
64
+ "three": "^0.177.0",
65
+ "urdf-loader": "^0.12.6",
66
+ "vaul": "^0.9.3",
67
+ "zod": "^3.23.8"
68
+ },
69
+ "devDependencies": {
70
+ "@eslint/js": "^9.9.0",
71
+ "@tailwindcss/typography": "^0.5.15",
72
+ "@types/node": "^22.5.5",
73
+ "@types/react": "^18.3.3",
74
+ "@types/react-dom": "^18.3.0",
75
+ "@vitejs/plugin-react-swc": "^4.3.1",
76
+ "autoprefixer": "^10.4.20",
77
+ "eslint": "^9.9.0",
78
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
79
+ "eslint-plugin-react-refresh": "^0.4.9",
80
+ "globals": "^15.9.0",
81
+ "lovable-tagger": "^1.3.0",
82
+ "postcss": "^8.5.15",
83
+ "tailwindcss": "^3.4.11",
84
+ "typescript": "^5.5.3",
85
+ "typescript-eslint": "^8.0.1",
86
+ "vite": "^8.0.16"
87
+ }
88
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/favicon.ico ADDED
public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png ADDED
public/placeholder.svg ADDED
public/robots.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ User-agent: Googlebot
2
+ Allow: /
3
+
4
+ User-agent: Bingbot
5
+ Allow: /
6
+
7
+ User-agent: Twitterbot
8
+ Allow: /
9
+
10
+ User-agent: facebookexternalhit
11
+ Allow: /
12
+
13
+ User-agent: *
14
+ Allow: /
public/so-101-urdf/CMakeLists.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.10.2)
2
+
3
+ project(so_arm_description)
4
+
5
+ find_package(ament_cmake REQUIRED)
6
+ find_package(urdf REQUIRED)
7
+
8
+ # Install the mesh files from SO101/assets
9
+ install(
10
+ DIRECTORY
11
+ SO101/assets/
12
+ DESTINATION
13
+ share/${PROJECT_NAME}/meshes
14
+ FILES_MATCHING PATTERN "*.stl"
15
+ )
16
+
17
+ # Install URDF files
18
+ install(
19
+ DIRECTORY
20
+ urdf/
21
+ DESTINATION
22
+ share/${PROJECT_NAME}/urdf
23
+ FILES_MATCHING PATTERN "*.urdf"
24
+ )
25
+
26
+ # Install other directories
27
+ install(
28
+ DIRECTORY
29
+ meshes
30
+ config
31
+ launch
32
+ DESTINATION
33
+ share/${PROJECT_NAME}
34
+ OPTIONAL
35
+ )
36
+
37
+ ament_package()
public/so-101-urdf/README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SO-ARM ROS2 URDF Package
2
+
3
+ A complete ROS2 package for the SO-ARM101 robotic arm with URDF description.
4
+
5
+ ## 📋 Overview
6
+
7
+ This package provides a complete ROS2 implementation for the SO-ARM101 robotic arm, including:
8
+
9
+ - URDF robot description with visual and collision meshes
10
+ - RViz visualization with pre-configured displays
11
+ - Launch files for easy robot visualization
12
+ - Integration with MoveIt for motion planning
13
+ - Joint state publishers for interactive control
14
+
15
+ ## 🎯 Original Source
16
+
17
+ https://github.com/TheRobotStudio/SO-ARM100/tree/main/Simulation/SO101
18
+
19
+ ## 🚀 Key Improvements Made
20
+
21
+ ### 1. **Complete ROS2 Package Structure**
22
+
23
+ - ✅ Proper `package.xml` with all necessary dependencies
24
+ - ✅ CMakeLists.txt for ROS2 build system
25
+ - ✅ Organized directory structure following ROS2 conventions
26
+
27
+ ### 2. **Enhanced Visualization**
28
+
29
+ - ✅ Fixed mesh file paths for proper package integration
30
+
31
+ ### Build Instructions
32
+
33
+ 1. Clone this repository into your ROS2 workspace:
34
+
35
+ ```bash
36
+ cd ~/your_ros2_ws/src
37
+ git clone <your-repo-url> so_arm_description
38
+ ```
39
+
40
+ 2. Build the package:
41
+ ```bash
42
+ cd ~/your_ros2_ws
43
+ colcon build --packages-select so_arm_description
44
+ source install/setup.bash
45
+ ```
public/so-101-urdf/config/joint_names_so_arm_urdf.yaml ADDED
@@ -0,0 +1 @@
 
 
1
+ controller_joint_names: ['', 'Rotation', 'Pitch', 'Elbow', 'Wrist_Pitch', 'Wrist_Roll', 'Jaw', ]
public/so-101-urdf/joints_properties.xml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <default>
2
+ <default class="sts3215">
3
+ <geom contype="0" conaffinity="0"/>
4
+ <joint damping="0.60" frictionloss="0.052" armature="0.028"/>
5
+ <position kp="17.8" kv="0.0" forcerange="-3.35 3.35"/>
6
+ </default>
7
+ <default class="backlash">
8
+ <!-- +/- 0.5° of backlash -->
9
+ <joint damping="0.01" frictionloss="0" armature="0.01" limited="true"
10
+ range="-0.008726646259971648 0.008726646259971648"/>
11
+ </default>
12
+ </default>
public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424
3
+ size 1877084
public/so-101-urdf/meshes/base_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d
3
+ size 471584
public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b
3
+ size 1129384
public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011
3
+ size 1052184
public/so-101-urdf/meshes/moving_jaw_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d
3
+ size 1413584
public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34
3
+ size 883684
public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887
3
+ size 865884
public/so-101-urdf/meshes/sts3215_03a_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0
3
+ size 954084
public/so-101-urdf/meshes/under_arm_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71
3
+ size 1975884
public/so-101-urdf/meshes/upper_arm_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f
3
+ size 1303484
public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9
3
+ size 62784
public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4
3
+ size 1439884
public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae
3
+ size 2699784
public/so-101-urdf/package.xml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3
+ <package format="3">
4
+ <name>so_arm_description</name>
5
+ <version>1.0.0</version>
6
+ <description>SO-ARM101 URDF Resources</description>
7
+
8
+ <author email="contact@lycheeai-hub.com">LycheeAI</author>
9
+
10
+ <maintainer email="contact@lycheeai-hub.com">LycheeAI</maintainer>
11
+
12
+ <license>BSD</license>
13
+
14
+ <buildtool_depend>ament_cmake</buildtool_depend>
15
+
16
+ <depend>urdf</depend>
17
+ <exec_depend>robot_state_publisher</exec_depend>
18
+ <exec_depend>joint_state_publisher</exec_depend>
19
+ <exec_depend>joint_state_publisher_gui</exec_depend>
20
+ <exec_depend>rviz2</exec_depend>
21
+ <exec_depend>xacro</exec_depend>
22
+
23
+ <export>
24
+ <build_type>ament_cmake</build_type>
25
+ </export>
26
+ </package>
public/so-101-urdf/urdf/so101_new_calib.urdf ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <!-- Generated using onshape-to-robot -->
3
+ <!-- Onshape https://cad.onshape.com/documents/7715cc284bb430fe6dab4ffd/w/4fd0791b683777b02f8d975a/e/826c553ede3b7592eb9ca800 -->
4
+ <robot name="so101_new_calib">
5
+
6
+ <!-- Materials -->
7
+ <material name="3d_printed">
8
+ <color rgba="1.0 0.82 0.12 1.0"/>
9
+ </material>
10
+ <material name="sts3215">
11
+ <color rgba="0.1 0.1 0.1 1.0"/>
12
+ </material>
13
+
14
+ <!-- Link base -->
15
+ <link name="base">
16
+ <inertial>
17
+ <origin xyz="0.020739 0.00204287 0.065966" rpy="0 0 0"/>
18
+ <mass value="0.147"/>
19
+ <inertia ixx="0.000136117" ixy="4.59787e-07" ixz="9.75275e-08" iyy="0.000114686" iyz="-4.97151e-06" izz="0.000130364"/>
20
+ </inertial>
21
+ <!-- Part base_motor_holder_so101_v1 -->
22
+ <visual>
23
+ <origin xyz="0.0206915 0.0221255 0.0300817" rpy="1.5708 -1.23909e-16 2.33147e-15"/>
24
+ <geometry>
25
+ <mesh filename="package://so_arm_description/meshes/base_motor_holder_so101_v1.stl"/>
26
+ </geometry>
27
+ <material name="3d_printed"/>
28
+ </visual>
29
+ <collision>
30
+ <origin xyz="0.0206915 0.0221255 0.0300817" rpy="1.5708 -1.23909e-16 2.33147e-15"/>
31
+ <geometry>
32
+ <mesh filename="package://so_arm_description/meshes/base_motor_holder_so101_v1.stl"/>
33
+ </geometry>
34
+ </collision>
35
+ <!-- Part base_so101_v2 -->
36
+ <visual>
37
+ <origin xyz="0.0207909 0.0221255 0.0300817" rpy="1.5708 -0 0"/>
38
+ <geometry>
39
+ <mesh filename="package://so_arm_description/meshes/base_so101_v2.stl"/>
40
+ </geometry>
41
+ <material name="3d_printed"/>
42
+ </visual>
43
+ <collision>
44
+ <origin xyz="0.0207909 0.0221255 0.0300817" rpy="1.5708 -0 0"/>
45
+ <geometry>
46
+ <mesh filename="package://so_arm_description/meshes/base_so101_v2.stl"/>
47
+ </geometry>
48
+ </collision>
49
+ <!-- Part sts3215_03a_v1 -->
50
+ <visual>
51
+ <origin xyz="0.0207909 -0.0105745 0.0761817" rpy="-2.20282e-15 2.77556e-17 -1.5708"/>
52
+ <geometry>
53
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
54
+ </geometry>
55
+ <material name="sts3215"/>
56
+ </visual>
57
+ <collision>
58
+ <origin xyz="0.0207909 -0.0105745 0.0761817" rpy="-2.20282e-15 2.77556e-17 -1.5708"/>
59
+ <geometry>
60
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
61
+ </geometry>
62
+ </collision>
63
+ <!-- Part waveshare_mounting_plate_so101_v2 -->
64
+ <visual>
65
+ <origin xyz="0.0205915 0.0467435 0.0798817" rpy="1.5708 -1.21716e-14 2.33147e-15"/>
66
+ <geometry>
67
+ <mesh filename="package://so_arm_description/meshes/waveshare_mounting_plate_so101_v2.stl"/>
68
+ </geometry>
69
+ <material name="3d_printed"/>
70
+ </visual>
71
+ <collision>
72
+ <origin xyz="0.0205915 0.0467435 0.0798817" rpy="1.5708 -1.21716e-14 2.33147e-15"/>
73
+ <geometry>
74
+ <mesh filename="package://so_arm_description/meshes/waveshare_mounting_plate_so101_v2.stl"/>
75
+ </geometry>
76
+ </collision>
77
+ </link>
78
+
79
+ <!-- Link shoulder -->
80
+ <link name="shoulder">
81
+ <inertial>
82
+ <origin xyz="-0.0307604 -1.66727e-05 -0.0252713" rpy="0 0 0"/>
83
+ <mass value="0.100006"/>
84
+ <inertia ixx="8.3759e-05" ixy="7.55525e-08" ixz="-1.16342e-06" iyy="8.10403e-05" iyz="1.54663e-07" izz="2.39783e-05"/>
85
+ </inertial>
86
+ <!-- Part sts3215_03a_v1_2 -->
87
+ <visual>
88
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
89
+ <geometry>
90
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
91
+ </geometry>
92
+ <material name="sts3215"/>
93
+ </visual>
94
+ <collision>
95
+ <origin xyz="-0.0303992 0.000422241 -0.0417" rpy="1.5708 1.5708 0"/>
96
+ <geometry>
97
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
98
+ </geometry>
99
+ </collision>
100
+ <!-- Part motor_holder_so101_base_v1 -->
101
+ <visual>
102
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
103
+ <geometry>
104
+ <mesh filename="package://so_arm_description/meshes/motor_holder_so101_base_v1.stl"/>
105
+ </geometry>
106
+ <material name="3d_printed"/>
107
+ </visual>
108
+ <collision>
109
+ <origin xyz="-0.0675992 -0.000177759 0.0158499" rpy="1.5708 -1.5708 0"/>
110
+ <geometry>
111
+ <mesh filename="package://so_arm_description/meshes/motor_holder_so101_base_v1.stl"/>
112
+ </geometry>
113
+ </collision>
114
+ <!-- Part rotation_pitch_so101_v1 -->
115
+ <visual>
116
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 -0 0"/>
117
+ <geometry>
118
+ <mesh filename="package://so_arm_description/meshes/rotation_pitch_so101_v1.stl"/>
119
+ </geometry>
120
+ <material name="3d_printed"/>
121
+ </visual>
122
+ <collision>
123
+ <origin xyz="0.0122008 2.22413e-05 0.0464" rpy="-1.5708 -0 0"/>
124
+ <geometry>
125
+ <mesh filename="package://so_arm_description/meshes/rotation_pitch_so101_v1.stl"/>
126
+ </geometry>
127
+ </collision>
128
+ </link>
129
+
130
+ <!-- Link upper_arm -->
131
+ <link name="upper_arm">
132
+ <inertial>
133
+ <origin xyz="-0.0898471 -0.00838224 0.0184089" rpy="0 0 0"/>
134
+ <mass value="0.103"/>
135
+ <inertia ixx="4.08002e-05" ixy="-1.97819e-05" ixz="-4.03016e-08" iyy="0.000147318" iyz="8.97326e-09" izz="0.000142487"/>
136
+ </inertial>
137
+ <!-- Part sts3215_03a_v1_3 -->
138
+ <visual>
139
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -6.8695e-16 -1.5708"/>
140
+ <geometry>
141
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
142
+ </geometry>
143
+ <material name="sts3215"/>
144
+ </visual>
145
+ <collision>
146
+ <origin xyz="-0.11257 -0.0155 0.0187" rpy="-3.14159 -6.8695e-16 -1.5708"/>
147
+ <geometry>
148
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
149
+ </geometry>
150
+ </collision>
151
+ <!-- Part upper_arm_so101_v1 -->
152
+ <visual>
153
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -9.35612e-32 0"/>
154
+ <geometry>
155
+ <mesh filename="package://so_arm_description/meshes/upper_arm_so101_v1.stl"/>
156
+ </geometry>
157
+ <material name="3d_printed"/>
158
+ </visual>
159
+ <collision>
160
+ <origin xyz="-0.065085 0.012 0.0182" rpy="3.14159 -9.35612e-32 0"/>
161
+ <geometry>
162
+ <mesh filename="package://so_arm_description/meshes/upper_arm_so101_v1.stl"/>
163
+ </geometry>
164
+ </collision>
165
+ </link>
166
+
167
+ <!-- Link lower_arm -->
168
+ <link name="lower_arm">
169
+ <inertial>
170
+ <origin xyz="-0.0980701 0.00324376 0.0182831" rpy="0 0 0"/>
171
+ <mass value="0.104"/>
172
+ <inertia ixx="2.87438e-05" ixy="7.41152e-06" ixz="1.26409e-06" iyy="0.000159844" iyz="-4.90188e-08" izz="0.00014529"/>
173
+ </inertial>
174
+ <!-- Part under_arm_so101_v1 -->
175
+ <visual>
176
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="-3.14159 -0 3.9443e-31"/>
177
+ <geometry>
178
+ <mesh filename="package://so_arm_description/meshes/under_arm_so101_v1.stl"/>
179
+ </geometry>
180
+ <material name="3d_printed"/>
181
+ </visual>
182
+ <collision>
183
+ <origin xyz="-0.0648499 -0.032 0.0182" rpy="-3.14159 -0 3.9443e-31"/>
184
+ <geometry>
185
+ <mesh filename="package://so_arm_description/meshes/under_arm_so101_v1.stl"/>
186
+ </geometry>
187
+ </collision>
188
+ <!-- Part motor_holder_so101_wrist_v1 -->
189
+ <visual>
190
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 4.73317e-30 7.88861e-31"/>
191
+ <geometry>
192
+ <mesh filename="package://so_arm_description/meshes/motor_holder_so101_wrist_v1.stl"/>
193
+ </geometry>
194
+ <material name="3d_printed"/>
195
+ </visual>
196
+ <collision>
197
+ <origin xyz="-0.0648499 -0.032 0.018" rpy="-3.14159 4.73317e-30 7.88861e-31"/>
198
+ <geometry>
199
+ <mesh filename="package://so_arm_description/meshes/motor_holder_so101_wrist_v1.stl"/>
200
+ </geometry>
201
+ </collision>
202
+ <!-- Part sts3215_03a_v1_4 -->
203
+ <visual>
204
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -3.58047e-15 -3.14159"/>
205
+ <geometry>
206
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
207
+ </geometry>
208
+ <material name="sts3215"/>
209
+ </visual>
210
+ <collision>
211
+ <origin xyz="-0.1224 0.0052 0.0187" rpy="-3.14159 -3.58047e-15 -3.14159"/>
212
+ <geometry>
213
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
214
+ </geometry>
215
+ </collision>
216
+ </link>
217
+
218
+ <!-- Link wrist -->
219
+ <link name="wrist">
220
+ <inertial>
221
+ <origin xyz="-0.000103312 -0.0386143 0.0281156" rpy="0 0 0"/>
222
+ <mass value="0.079"/>
223
+ <inertia ixx="3.68263e-05" ixy="1.7893e-08" ixz="-5.28128e-08" iyy="2.5391e-05" iyz="3.6412e-06" izz="2.1e-05"/>
224
+ </inertial>
225
+ <!-- Part sts3215_03a_no_horn_v1 -->
226
+ <visual>
227
+ <origin xyz="5.55112e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
228
+ <geometry>
229
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_no_horn_v1.stl"/>
230
+ </geometry>
231
+ <material name="sts3215"/>
232
+ </visual>
233
+ <collision>
234
+ <origin xyz="5.55112e-17 -0.0424 0.0306" rpy="1.5708 1.5708 0"/>
235
+ <geometry>
236
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_no_horn_v1.stl"/>
237
+ </geometry>
238
+ </collision>
239
+ <!-- Part wrist_roll_pitch_so101_v2 -->
240
+ <visual>
241
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
242
+ <geometry>
243
+ <mesh filename="package://so_arm_description/meshes/wrist_roll_pitch_so101_v2.stl"/>
244
+ </geometry>
245
+ <material name="3d_printed"/>
246
+ </visual>
247
+ <collision>
248
+ <origin xyz="0 -0.028 0.0181" rpy="-1.5708 -1.5708 0"/>
249
+ <geometry>
250
+ <mesh filename="package://so_arm_description/meshes/wrist_roll_pitch_so101_v2.stl"/>
251
+ </geometry>
252
+ </collision>
253
+ </link>
254
+
255
+ <!-- Link gripper -->
256
+ <link name="gripper">
257
+ <inertial>
258
+ <origin xyz="0.000213627 0.000245138 -0.025187" rpy="0 0 0"/>
259
+ <mass value="0.087"/>
260
+ <inertia ixx="2.75087e-05" ixy="-3.35241e-07" ixz="-5.7352e-06" iyy="4.33657e-05" iyz="-5.17847e-08" izz="3.45059e-05"/>
261
+ </inertial>
262
+ <!-- Part sts3215_03a_v1_5 -->
263
+ <visual>
264
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.55112e-17 -1.38213e-14"/>
265
+ <geometry>
266
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
267
+ </geometry>
268
+ <material name="sts3215"/>
269
+ </visual>
270
+ <collision>
271
+ <origin xyz="0.0077 0.0001 -0.0234" rpy="-1.5708 -5.55112e-17 -1.38213e-14"/>
272
+ <geometry>
273
+ <mesh filename="package://so_arm_description/meshes/sts3215_03a_v1.stl"/>
274
+ </geometry>
275
+ </collision>
276
+ <!-- Part wrist_roll_follower_so101_v1 -->
277
+ <visual>
278
+ <origin xyz="5.55112e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 -9.17912e-24"/>
279
+ <geometry>
280
+ <mesh filename="package://so_arm_description/meshes/wrist_roll_follower_so101_v1.stl"/>
281
+ </geometry>
282
+ <material name="3d_printed"/>
283
+ </visual>
284
+ <collision>
285
+ <origin xyz="5.55112e-17 -0.000218214 0.000949706" rpy="-3.14159 -5.55112e-17 -9.17912e-24"/>
286
+ <geometry>
287
+ <mesh filename="package://so_arm_description/meshes/wrist_roll_follower_so101_v1.stl"/>
288
+ </geometry>
289
+ </collision>
290
+ </link>
291
+
292
+ <!-- Link jaw -->
293
+ <link name="jaw">
294
+ <inertial>
295
+ <origin xyz="-0.00157495 -0.0300244 0.0192755" rpy="0 0 0"/>
296
+ <mass value="0.012"/>
297
+ <inertia ixx="6.61427e-06" ixy="-3.19807e-07" ixz="-5.90717e-09" iyy="1.89032e-06" iyz="-1.09945e-07" izz="5.28738e-06"/>
298
+ </inertial>
299
+ <!-- Part moving_jaw_so101_v1 -->
300
+ <visual>
301
+ <origin xyz="-5.55112e-17 -1.94746e-17 0.0189" rpy="9.53145e-17 -4.66093e-24 0"/>
302
+ <geometry>
303
+ <mesh filename="package://so_arm_description/meshes/moving_jaw_so101_v1.stl"/>
304
+ </geometry>
305
+ <material name="3d_printed"/>
306
+ </visual>
307
+ <collision>
308
+ <origin xyz="-5.55112e-17 -1.94746e-17 0.0189" rpy="9.53145e-17 -4.66093e-24 0"/>
309
+ <geometry>
310
+ <mesh filename="package://so_arm_description/meshes/moving_jaw_so101_v1.stl"/>
311
+ </geometry>
312
+ </collision>
313
+ </link>
314
+
315
+ <!-- Joint from gripper to jaw -->
316
+ <joint name="Jaw" type="revolute">
317
+ <origin xyz="0.0202 0.0188 -0.0234" rpy="1.5708 -5.14108e-17 -1.38655e-14"/>
318
+ <parent link="gripper"/>
319
+ <child link="jaw"/>
320
+ <axis xyz="0 0 1"/>
321
+ <limit effort="10" velocity="10" lower="-0.174533" upper="1.74533"/>
322
+ </joint>
323
+
324
+ <transmission name="6_trans">
325
+ <type>transmission_interface/SimpleTransmission</type>
326
+ <joint name="Jaw">
327
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
328
+ </joint>
329
+ <actuator name="motor6">
330
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
331
+ <mechanicalReduction>1</mechanicalReduction>
332
+ </actuator>
333
+ </transmission>
334
+
335
+ <!-- Joint from wrist to gripper -->
336
+ <joint name="Wrist_Roll" type="revolute">
337
+ <origin xyz="0 -0.0611 0.0181" rpy="1.5708 -9.38083e-08 3.14159"/>
338
+ <parent link="wrist"/>
339
+ <child link="gripper"/>
340
+ <axis xyz="0 0 1"/>
341
+ <limit effort="10" velocity="10" lower="-2.79253" upper="2.79253"/>
342
+ </joint>
343
+
344
+ <transmission name="5_trans">
345
+ <type>transmission_interface/SimpleTransmission</type>
346
+ <joint name="Wrist_Roll">
347
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
348
+ </joint>
349
+ <actuator name="motor5">
350
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
351
+ <mechanicalReduction>1</mechanicalReduction>
352
+ </actuator>
353
+ </transmission>
354
+
355
+ <!-- Joint from lower_arm to wrist -->
356
+ <joint name="Wrist_Pitch" type="revolute">
357
+ <origin xyz="-0.1349 0.0052 1.65232e-16" rpy="3.2474e-15 2.86219e-15 -1.5708"/>
358
+ <parent link="lower_arm"/>
359
+ <child link="wrist"/>
360
+ <axis xyz="0 0 1"/>
361
+ <limit effort="10" velocity="10" lower="-1.65806" upper="1.65806"/>
362
+ </joint>
363
+
364
+ <transmission name="4_trans">
365
+ <type>transmission_interface/SimpleTransmission</type>
366
+ <joint name="Wrist_Pitch">
367
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
368
+ </joint>
369
+ <actuator name="motor4">
370
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
371
+ <mechanicalReduction>1</mechanicalReduction>
372
+ </actuator>
373
+ </transmission>
374
+
375
+ <!-- Joint from upper_arm to lower_arm -->
376
+ <joint name="Elbow" type="revolute">
377
+ <origin xyz="-0.11257 -0.028 2.46331e-16" rpy="-1.22818e-15 5.75928e-16 1.5708"/>
378
+ <parent link="upper_arm"/>
379
+ <child link="lower_arm"/>
380
+ <axis xyz="0 0 1"/>
381
+ <limit effort="10" velocity="10" lower="-1.74533" upper="1.5708"/>
382
+ </joint>
383
+
384
+ <transmission name="3_trans">
385
+ <type>transmission_interface/SimpleTransmission</type>
386
+ <joint name="Elbow">
387
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
388
+ </joint>
389
+ <actuator name="motor3">
390
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
391
+ <mechanicalReduction>1</mechanicalReduction>
392
+ </actuator>
393
+ </transmission>
394
+
395
+ <!-- Joint from shoulder to upper_arm -->
396
+ <joint name="Pitch" type="revolute">
397
+ <origin xyz="-0.0303992 -0.0182778 -0.0542" rpy="-1.5708 -1.5708 0"/>
398
+ <parent link="shoulder"/>
399
+ <child link="upper_arm"/>
400
+ <axis xyz="0 0 1"/>
401
+ <limit effort="10" velocity="10" lower="-1.74533" upper="1.74533"/>
402
+ </joint>
403
+
404
+ <transmission name="2_trans">
405
+ <type>transmission_interface/SimpleTransmission</type>
406
+ <joint name="Pitch">
407
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
408
+ </joint>
409
+ <actuator name="motor2">
410
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
411
+ <mechanicalReduction>1</mechanicalReduction>
412
+ </actuator>
413
+ </transmission>
414
+
415
+ <!-- Joint from base to shoulder -->
416
+ <joint name="Rotation" type="revolute">
417
+ <origin xyz="0.0207909 -0.0230745 0.0948817" rpy="-3.14159 6.03684e-16 1.5708"/>
418
+ <parent link="base"/>
419
+ <child link="shoulder"/>
420
+ <axis xyz="0 0 1"/>
421
+ <limit effort="10" velocity="10" lower="-1.91986" upper="1.91986"/>
422
+ </joint>
423
+
424
+ <transmission name="1_trans">
425
+ <type>transmission_interface/SimpleTransmission</type>
426
+ <joint name="Rotation">
427
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
428
+ </joint>
429
+ <actuator name="motor1">
430
+ <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
431
+ <mechanicalReduction>1</mechanicalReduction>
432
+ </actuator>
433
+ </transmission>
434
+
435
+ </robot>
src/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ node_modules/
src/App.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { ThemeProvider } from "@/contexts/ThemeContext";
4
+ import { UrdfProvider } from "@/contexts/UrdfContext";
5
+ import { DragAndDropProvider } from "@/contexts/DragAndDropContext";
6
+ import { Toaster } from "@/components/ui/toaster";
7
+ import Landing from "@/pages/Landing";
8
+ import Teleoperation from "@/pages/Teleoperation";
9
+ import Calibration from "@/pages/Calibration";
10
+ import Recording from "@/pages/Recording";
11
+ import Training from "@/pages/Training";
12
+ import Inference from "@/pages/Inference";
13
+ import EditDataset from "@/pages/EditDataset";
14
+ import Upload from "@/pages/Upload";
15
+
16
+ import NotFound from "@/pages/NotFound";
17
+ import SingleTabGuard from "@/components/SingleTabGuard";
18
+ import TeleopStopNotice from "@/components/TeleopStopNotice";
19
+ import UpdateNotice from "@/components/UpdateNotice";
20
+ import { TooltipProvider } from "@radix-ui/react-tooltip";
21
+ import { ApiProvider } from "./contexts/ApiContext";
22
+ import { HfAuthProvider } from "./contexts/HfAuthContext";
23
+
24
+ const queryClient = new QueryClient();
25
+
26
+ function App() {
27
+ return (
28
+ <QueryClientProvider client={queryClient}>
29
+ <TooltipProvider>
30
+ <ThemeProvider>
31
+ <ApiProvider>
32
+ <HfAuthProvider>
33
+ <UrdfProvider>
34
+ <DragAndDropProvider>
35
+ <BrowserRouter>
36
+ <SingleTabGuard>
37
+ <TeleopStopNotice />
38
+ <UpdateNotice />
39
+ <Routes>
40
+ <Route path="/" element={<Landing />} />
41
+ <Route path="/teleoperation" element={<Teleoperation />} />
42
+ <Route path="/recording" element={<Recording />} />
43
+ <Route path="/upload" element={<Upload />} />
44
+ <Route path="/training" element={<Training />} />
45
+ <Route path="/training/:jobId" element={<Training />} />
46
+ <Route path="/inference" element={<Inference />} />
47
+ <Route path="/calibration" element={<Calibration />} />
48
+ <Route path="/edit-dataset" element={<EditDataset />} />
49
+
50
+ <Route path="*" element={<NotFound />} />
51
+ </Routes>
52
+ </SingleTabGuard>
53
+ <Toaster />
54
+ </BrowserRouter>
55
+ </DragAndDropProvider>
56
+ </UrdfProvider>
57
+ </HfAuthProvider>
58
+ </ApiProvider>
59
+ </ThemeProvider>
60
+ </TooltipProvider>
61
+ </QueryClientProvider>
62
+ );
63
+ }
64
+
65
+ export default App;
src/components/Footer.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Github, BookOpen } from "lucide-react";
3
+
4
+ const DiscordIcon: React.FC<{ className?: string }> = ({ className }) => (
5
+ <svg
6
+ role="img"
7
+ viewBox="0 0 24 24"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ fill="currentColor"
10
+ className={className}
11
+ >
12
+ <path d="M20.317 4.369A19.79 19.79 0 0 0 16.558 3.2a.07.07 0 0 0-.074.035c-.211.375-.444.864-.608 1.249a18.27 18.27 0 0 0-5.487 0 12.51 12.51 0 0 0-.617-1.249.077.077 0 0 0-.074-.035 19.736 19.736 0 0 0-3.76 1.169.07.07 0 0 0-.032.027C2.533 8.046 1.79 11.624 2.155 15.157a.082.082 0 0 0 .031.056 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.027c.462-.63.873-1.295 1.226-1.994a.076.076 0 0 0-.041-.105 13.13 13.13 0 0 1-1.873-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .077-.01c3.927 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .078.009c.12.099.246.198.373.292a.077.077 0 0 1-.006.128 12.32 12.32 0 0 1-1.873.891.077.077 0 0 0-.04.106c.36.698.772 1.363 1.225 1.993a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-4.087-.838-7.636-3.548-10.787a.061.061 0 0 0-.031-.028zM8.02 12.997c-1.182 0-2.156-1.085-2.156-2.419 0-1.333.955-2.419 2.156-2.419 1.21 0 2.175 1.095 2.156 2.42 0 1.333-.955 2.418-2.156 2.418zm7.974 0c-1.182 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.175 1.095 2.156 2.42 0 1.333-.946 2.418-2.156 2.418z" />
13
+ </svg>
14
+ );
15
+
16
+ const links = [
17
+ {
18
+ href: "https://github.com/huggingface/lerobot",
19
+ label: "GitHub",
20
+ Icon: Github,
21
+ },
22
+ {
23
+ href: "https://huggingface.co/docs/lerobot",
24
+ label: "Documentation",
25
+ Icon: BookOpen,
26
+ },
27
+ {
28
+ href: "https://discord.com/invite/s3KuuzsPFb",
29
+ label: "Discord",
30
+ Icon: DiscordIcon,
31
+ },
32
+ ];
33
+
34
+ const Footer: React.FC = () => {
35
+ return (
36
+ <footer className="fixed inset-x-0 bottom-0 z-30 border-t border-gray-800 bg-black/95">
37
+ <div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-3 px-4 py-4 text-sm text-gray-400 sm:flex-row">
38
+ <span>
39
+ Powered by{" "}
40
+ <a
41
+ href="https://github.com/huggingface/lerobot"
42
+ target="_blank"
43
+ rel="noopener noreferrer"
44
+ className="font-medium text-gray-200 hover:text-white"
45
+ >
46
+ LeRobot
47
+ </a>
48
+ </span>
49
+ <nav className="flex items-center gap-4">
50
+ {links.map(({ href, label, Icon }) => (
51
+ <a
52
+ key={label}
53
+ href={href}
54
+ target="_blank"
55
+ rel="noopener noreferrer"
56
+ className="flex items-center gap-1.5 text-gray-400 hover:text-white"
57
+ >
58
+ <Icon className="h-4 w-4" />
59
+ <span>{label}</span>
60
+ </a>
61
+ ))}
62
+ </nav>
63
+ </div>
64
+ </footer>
65
+ );
66
+ };
67
+
68
+ export default Footer;
src/components/Logo.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ interface LogoProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ iconOnly?: boolean;
7
+ }
8
+
9
+ const Logo: React.FC<LogoProps> = ({
10
+ className,
11
+ iconOnly = false
12
+ }) => {
13
+ return <div className={cn("flex items-center gap-2", className)}>
14
+ <img src="/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png" alt="LeLab Logo" className="h-8 w-8" />
15
+ {!iconOnly && <span className="font-bold text-white text-2xl">LeLab</span>}
16
+ </div>;
17
+ };
18
+
19
+ export default Logo;
src/components/SingleTabGuard.tsx ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useRef, useState, ReactNode } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+
4
+ type Peer = { id: string; openedAt: number; lastSeen: number };
5
+
6
+ const CHANNEL = "lelab-tabs-v1";
7
+ const HEARTBEAT_MS = 1000;
8
+ const PEER_TIMEOUT_MS = 3000;
9
+
10
+ const SingleTabGuard = ({ children }: { children: ReactNode }) => {
11
+ const [isPrimary, setIsPrimary] = useState(true);
12
+ const peersRef = useRef<Map<string, Peer>>(new Map());
13
+ const myIdRef = useRef<string>("");
14
+ const myOpenedAtRef = useRef<number>(0);
15
+ const channelRef = useRef<BroadcastChannel | null>(null);
16
+
17
+ const recompute = useCallback(() => {
18
+ const peers = peersRef.current;
19
+ const cutoff = Date.now() - PEER_TIMEOUT_MS;
20
+ for (const [id, peer] of peers) {
21
+ if (peer.lastSeen < cutoff) peers.delete(id);
22
+ }
23
+ let winnerId = myIdRef.current;
24
+ let winnerOpenedAt = myOpenedAtRef.current;
25
+ for (const peer of peers.values()) {
26
+ if (
27
+ peer.openedAt < winnerOpenedAt ||
28
+ (peer.openedAt === winnerOpenedAt && peer.id < winnerId)
29
+ ) {
30
+ winnerId = peer.id;
31
+ winnerOpenedAt = peer.openedAt;
32
+ }
33
+ }
34
+ setIsPrimary(winnerId === myIdRef.current);
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ if (typeof window === "undefined" || typeof BroadcastChannel === "undefined") {
39
+ return;
40
+ }
41
+
42
+ myIdRef.current = crypto.randomUUID();
43
+ myOpenedAtRef.current = Date.now();
44
+
45
+ const channel = new BroadcastChannel(CHANNEL);
46
+ channelRef.current = channel;
47
+
48
+ const send = (type: string) => {
49
+ channel.postMessage({
50
+ type,
51
+ id: myIdRef.current,
52
+ openedAt: myOpenedAtRef.current,
53
+ });
54
+ };
55
+
56
+ channel.onmessage = (e) => {
57
+ const msg = e.data;
58
+ if (!msg || msg.id === myIdRef.current) return;
59
+ const peers = peersRef.current;
60
+
61
+ if (msg.type === "HEARTBEAT") {
62
+ peers.set(msg.id, {
63
+ id: msg.id,
64
+ openedAt: msg.openedAt,
65
+ lastSeen: Date.now(),
66
+ });
67
+ } else if (msg.type === "RELEASE") {
68
+ peers.delete(msg.id);
69
+ } else if (msg.type === "TAKEOVER") {
70
+ peers.set(msg.id, {
71
+ id: msg.id,
72
+ openedAt: msg.openedAt,
73
+ lastSeen: Date.now(),
74
+ });
75
+ // Move ourselves behind the taker so the election flips.
76
+ if (myOpenedAtRef.current <= msg.openedAt) {
77
+ myOpenedAtRef.current = msg.openedAt + 1;
78
+ }
79
+ }
80
+ recompute();
81
+ };
82
+
83
+ send("HEARTBEAT");
84
+ const interval = setInterval(() => {
85
+ send("HEARTBEAT");
86
+ recompute();
87
+ }, HEARTBEAT_MS);
88
+
89
+ const onUnload = () => send("RELEASE");
90
+ window.addEventListener("beforeunload", onUnload);
91
+
92
+ return () => {
93
+ window.removeEventListener("beforeunload", onUnload);
94
+ clearInterval(interval);
95
+ send("RELEASE");
96
+ channel.close();
97
+ channelRef.current = null;
98
+ };
99
+ }, [recompute]);
100
+
101
+ const takeOver = useCallback(() => {
102
+ myOpenedAtRef.current = 0;
103
+ channelRef.current?.postMessage({
104
+ type: "TAKEOVER",
105
+ id: myIdRef.current,
106
+ openedAt: 0,
107
+ });
108
+ recompute();
109
+ }, [recompute]);
110
+
111
+ return (
112
+ <>
113
+ {children}
114
+ {!isPrimary && (
115
+ <div
116
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm"
117
+ role="dialog"
118
+ aria-modal="true"
119
+ >
120
+ <div className="mx-4 max-w-md space-y-4 rounded-lg border bg-background p-6 text-center shadow-lg">
121
+ <h2 className="text-lg font-semibold">
122
+ LeLab is already open in another tab
123
+ </h2>
124
+ <p className="text-sm text-muted-foreground">
125
+ Only one tab can control the robot at a time. Switch back to the
126
+ original tab, or take over here — the other tab will lock.
127
+ </p>
128
+ <Button onClick={takeOver}>Use this tab</Button>
129
+ </div>
130
+ </div>
131
+ )}
132
+ </>
133
+ );
134
+ };
135
+
136
+ export default SingleTabGuard;
src/components/TeleopStopNotice.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from "react";
2
+ import { useToast } from "@/hooks/use-toast";
3
+
4
+ const FLAG = "lelab:teleop-stopped";
5
+
6
+ /**
7
+ * One-time confirmation that teleoperation was stopped during the previous
8
+ * page's unload (a browser navigation, reload, or tab close from the
9
+ * teleoperation page set a sessionStorage flag, since React cleanup can't run
10
+ * in those cases). On the next fresh load we surface a toast wherever the user
11
+ * landed, then clear the flag. In-app navigation away from teleop toasts
12
+ * directly and never sets the flag, so this never double-fires.
13
+ */
14
+ const TeleopStopNotice = () => {
15
+ const { toast } = useToast();
16
+
17
+ useEffect(() => {
18
+ let stopped = false;
19
+ try {
20
+ stopped = sessionStorage.getItem(FLAG) === "1";
21
+ if (stopped) sessionStorage.removeItem(FLAG);
22
+ } catch {
23
+ /* sessionStorage unavailable — nothing to show */
24
+ }
25
+ if (stopped) {
26
+ toast({
27
+ title: "Teleoperation stopped",
28
+ description: "The arm was disconnected cleanly when you left the page.",
29
+ });
30
+ }
31
+ }, [toast]);
32
+
33
+ return null;
34
+ };
35
+
36
+ export default TeleopStopNotice;
src/components/UpdateNotice.tsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Loader2, Copy, Sparkles, ChevronRight } from "lucide-react";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from "@/components/ui/dialog";
10
+ import {
11
+ Collapsible,
12
+ CollapsibleContent,
13
+ CollapsibleTrigger,
14
+ } from "@/components/ui/collapsible";
15
+ import { Button } from "@/components/ui/button";
16
+ import { Checkbox } from "@/components/ui/checkbox";
17
+ import { useApi } from "@/contexts/ApiContext";
18
+ import { useToast } from "@/hooks/use-toast";
19
+ import { useUpdateCheck } from "@/hooks/useUpdateCheck";
20
+
21
+ /**
22
+ * App-level popup that notifies the user when a newer LeLab is available on
23
+ * GitHub. Offers a copy-able upgrade command, a best-effort "Update now" button
24
+ * (runs the pip upgrade on the backend), and a "don't ask again" opt-out.
25
+ */
26
+ const UpdateNotice = () => {
27
+ const { status, open, dismiss } = useUpdateCheck();
28
+ const { baseUrl, fetchWithHeaders } = useApi();
29
+ const { toast } = useToast();
30
+ const [dontAsk, setDontAsk] = useState(false);
31
+ const [updating, setUpdating] = useState(false);
32
+ const [output, setOutput] = useState<string | null>(null);
33
+
34
+ if (!status) return null;
35
+
36
+ const behind =
37
+ typeof status.commits_behind === "number" && status.commits_behind > 0
38
+ ? `${status.commits_behind} commit${status.commits_behind === 1 ? "" : "s"} behind`
39
+ : "A new version is available";
40
+
41
+ const copyCommand = async () => {
42
+ if (!status.update_command) return;
43
+ try {
44
+ await navigator.clipboard.writeText(status.update_command);
45
+ toast({
46
+ title: "Copied",
47
+ description: "Update command copied to clipboard.",
48
+ });
49
+ } catch {
50
+ toast({
51
+ title: "Copy failed",
52
+ description: "Select and copy the command manually.",
53
+ variant: "destructive",
54
+ });
55
+ }
56
+ };
57
+
58
+ const runUpdate = async () => {
59
+ setUpdating(true);
60
+ setOutput(null);
61
+ try {
62
+ const r = await fetchWithHeaders(`${baseUrl}/system/update`, {
63
+ method: "POST",
64
+ });
65
+ const body: { success: boolean; message: string; output: string } =
66
+ await r.json();
67
+ if (body.success) {
68
+ toast({ title: "Updated", description: body.message });
69
+ dismiss(false);
70
+ } else {
71
+ setOutput(body.output || body.message);
72
+ toast({
73
+ title: "Update failed",
74
+ description: body.message,
75
+ variant: "destructive",
76
+ });
77
+ }
78
+ } catch (e) {
79
+ toast({
80
+ title: "Update failed",
81
+ description: e instanceof Error ? e.message : String(e),
82
+ variant: "destructive",
83
+ });
84
+ } finally {
85
+ setUpdating(false);
86
+ }
87
+ };
88
+
89
+ return (
90
+ <Dialog
91
+ open={open}
92
+ onOpenChange={(o) => {
93
+ if (!o && !updating) dismiss(dontAsk);
94
+ }}
95
+ >
96
+ <DialogContent
97
+ className="bg-slate-800 border-slate-700 text-white max-w-lg"
98
+ onOpenAutoFocus={(e) => e.preventDefault()}
99
+ >
100
+ <DialogHeader>
101
+ <DialogTitle className="flex items-center gap-3 text-white">
102
+ <Sparkles className="w-5 h-5 text-amber-400" />
103
+ LeLab update available
104
+ </DialogTitle>
105
+ <DialogDescription className="text-slate-300">
106
+ You're {behind} 😱.
107
+ <br />
108
+ Update to get the latest fixes and features 🤗.
109
+ {status.compare_url && (
110
+ <>
111
+ {" "}
112
+ <a
113
+ href={status.compare_url}
114
+ target="_blank"
115
+ rel="noreferrer"
116
+ className="text-sky-300 underline hover:text-sky-200"
117
+ >
118
+ See what changed
119
+ </a>
120
+ .
121
+ </>
122
+ )}
123
+ </DialogDescription>
124
+ </DialogHeader>
125
+
126
+ <div className="space-y-4">
127
+ <Collapsible>
128
+ <CollapsibleTrigger className="group flex items-center gap-1.5 text-xs font-medium text-slate-300 hover:text-white transition-colors">
129
+ <ChevronRight className="w-3.5 h-3.5 transition-transform group-data-[state=open]:rotate-90" />
130
+ Or update manually
131
+ </CollapsibleTrigger>
132
+ <CollapsibleContent className="pt-2">
133
+ <div className="flex items-start gap-2">
134
+ <code className="min-w-0 flex-1 px-2 py-1.5 rounded bg-slate-900 text-sky-300 text-xs break-all whitespace-pre-wrap">
135
+ {status.update_command}
136
+ </code>
137
+ <Button
138
+ variant="outline"
139
+ size="icon"
140
+ onClick={copyCommand}
141
+ title="Copy command"
142
+ className="shrink-0 bg-slate-900 border-slate-600 text-white hover:bg-slate-700"
143
+ >
144
+ <Copy className="w-4 h-4" />
145
+ </Button>
146
+ </div>
147
+ </CollapsibleContent>
148
+ </Collapsible>
149
+
150
+ {output && (
151
+ <pre className="max-h-40 overflow-auto rounded bg-slate-900 p-2 text-xs text-slate-300 whitespace-pre-wrap">
152
+ {output}
153
+ </pre>
154
+ )}
155
+
156
+ <div className="flex items-center justify-between gap-3 pt-1">
157
+ <label className="flex items-center gap-2 text-sm text-slate-300">
158
+ <Checkbox
159
+ checked={dontAsk}
160
+ onCheckedChange={(v) => setDontAsk(v === true)}
161
+ className="border-slate-300 data-[state=checked]:bg-slate-300 data-[state=checked]:text-slate-900"
162
+ />
163
+ Don't ask me again
164
+ </label>
165
+ <div className="flex items-center gap-2">
166
+ <Button
167
+ variant="ghost"
168
+ onClick={() => dismiss(dontAsk)}
169
+ disabled={updating}
170
+ className="text-slate-300 hover:bg-slate-700 hover:text-white"
171
+ >
172
+ Later
173
+ </Button>
174
+ {status.can_auto_update && (
175
+ <Button onClick={runUpdate} disabled={updating}>
176
+ {updating ? (
177
+ <>
178
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
179
+ Updating…
180
+ </>
181
+ ) : (
182
+ "Update now"
183
+ )}
184
+ </Button>
185
+ )}
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </DialogContent>
190
+ </Dialog>
191
+ );
192
+ };
193
+
194
+ export default UpdateNotice;
src/components/UrdfViewer.tsx ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ useMemo,
6
+ useCallback,
7
+ memo,
8
+ } from "react";
9
+ import { cn } from "@/lib/utils";
10
+
11
+ import URDFManipulator from "urdf-loader/src/urdf-manipulator-element.js";
12
+ import { useUrdf } from "@/hooks/useUrdf";
13
+ import { useRealTimeJoints } from "@/hooks/useRealTimeJoints";
14
+ import {
15
+ createUrdfViewer,
16
+ setupMeshLoader,
17
+ setupJointHighlighting,
18
+ setupModelLoading,
19
+ URDFViewerElement,
20
+ } from "@/lib/urdfViewerHelpers";
21
+
22
+ // Register the URDFManipulator as a custom element if it hasn't been already
23
+ if (typeof window !== "undefined" && !customElements.get("urdf-viewer")) {
24
+ customElements.define("urdf-viewer", URDFManipulator);
25
+ }
26
+ import * as THREE from "three";
27
+
28
+ // Extend the interface for the URDF viewer element to include background property
29
+ interface UrdfViewerElement extends HTMLElement {
30
+ background?: string;
31
+ setJointValue?: (jointName: string, value: number) => void;
32
+ }
33
+
34
+ const UrdfViewer: React.FC = () => {
35
+ const containerRef = useRef<HTMLDivElement>(null);
36
+ const [highlightedJoint, setHighlightedJoint] = useState<string | null>(null);
37
+ const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel } =
38
+ useUrdf();
39
+
40
+ const cleanupAnimationRef = useRef<(() => void) | null>(null);
41
+ const viewerRef = useRef<URDFViewerElement | null>(null);
42
+ const hasInitializedRef = useRef<boolean>(false);
43
+
44
+ // Real-time joint updates via WebSocket
45
+ const { isConnected: isWebSocketConnected } = useRealTimeJoints({
46
+ viewerRef,
47
+ enabled: isDefaultModel, // Only enable WebSocket for default model
48
+ });
49
+
50
+ // Add state for custom URDF path
51
+ const [customUrdfPath, setCustomUrdfPath] = useState<string | null>(null);
52
+ const [urlModifierFunc, setUrlModifierFunc] = useState<
53
+ ((url: string) => string) | null
54
+ >(null);
55
+
56
+ const packageRef = useRef<string>("");
57
+
58
+ // Implement UrdfProcessor interface for drag and drop
59
+ const urdfProcessor = useMemo(
60
+ () => ({
61
+ loadUrdf: (urdfPath: string) => {
62
+ setCustomUrdfPath(urdfPath);
63
+ },
64
+ setUrlModifierFunc: (func: (url: string) => string) => {
65
+ setUrlModifierFunc(() => func);
66
+ },
67
+ getPackage: () => {
68
+ return packageRef.current;
69
+ },
70
+ }),
71
+ []
72
+ );
73
+
74
+ // Register the URDF processor with the global drag and drop context
75
+ useEffect(() => {
76
+ registerUrdfProcessor(urdfProcessor);
77
+ }, [registerUrdfProcessor, urdfProcessor]);
78
+
79
+ // Create URL modifier function for default model
80
+ const defaultUrlModifier = useCallback((url: string) => {
81
+ console.log(`🔗 defaultUrlModifier called with: ${url}`);
82
+
83
+ // Handle various package:// URL formats for the default SO-101 model
84
+ if (url.startsWith("package://so_arm_description/meshes/")) {
85
+ const modifiedUrl = url.replace(
86
+ "package://so_arm_description/meshes/",
87
+ "/so-101-urdf/meshes/"
88
+ );
89
+ console.log(`🔗 Modified URL (package): ${modifiedUrl}`);
90
+ return modifiedUrl;
91
+ }
92
+
93
+ // Handle case where package path might be partially resolved
94
+ if (url.includes("so_arm_description/meshes/")) {
95
+ const modifiedUrl = url.replace(
96
+ /.*so_arm_description\/meshes\//,
97
+ "/so-101-urdf/meshes/"
98
+ );
99
+ console.log(`🔗 Modified URL (partial): ${modifiedUrl}`);
100
+ return modifiedUrl;
101
+ }
102
+
103
+ // Handle the specific problematic path pattern we're seeing in logs
104
+ if (url.includes("/so-101-urdf/so_arm_description/meshes/")) {
105
+ const modifiedUrl = url.replace(
106
+ "/so-101-urdf/so_arm_description/meshes/",
107
+ "/so-101-urdf/meshes/"
108
+ );
109
+ console.log(`🔗 Modified URL (problematic path): ${modifiedUrl}`);
110
+ return modifiedUrl;
111
+ }
112
+
113
+ // Handle relative paths that might need mesh folder prefix
114
+ if (
115
+ url.endsWith(".stl") &&
116
+ !url.startsWith("/") &&
117
+ !url.startsWith("http")
118
+ ) {
119
+ const modifiedUrl = `/so-101-urdf/meshes/${url}`;
120
+ console.log(`🔗 Modified URL (relative): ${modifiedUrl}`);
121
+ return modifiedUrl;
122
+ }
123
+
124
+ console.log(`🔗 Unmodified URL: ${url}`);
125
+ return url;
126
+ }, []);
127
+
128
+ // Main effect to create and setup the viewer only once
129
+ useEffect(() => {
130
+ if (!containerRef.current) return;
131
+
132
+ // Create and configure the URDF viewer element
133
+ const viewer = createUrdfViewer(containerRef.current, true);
134
+ viewerRef.current = viewer; // Store reference to the viewer
135
+
136
+ // Setup mesh loading function with appropriate URL modifier
137
+ const activeUrlModifier = isDefaultModel
138
+ ? defaultUrlModifier
139
+ : urlModifierFunc;
140
+ setupMeshLoader(viewer, activeUrlModifier);
141
+
142
+ // Determine which URDF to load - fixed path to match the actual available file
143
+ const urdfPath = isDefaultModel
144
+ ? "/so-101-urdf/urdf/so101_new_calib.urdf"
145
+ : customUrdfPath || "";
146
+
147
+ // Set the package path for the default model
148
+ if (isDefaultModel) {
149
+ packageRef.current = "/"; // Set to root so we can handle full path resolution in URL modifier
150
+ }
151
+
152
+ // Setup model loading if a path is available
153
+ let cleanupModelLoading = () => {};
154
+ if (urdfPath) {
155
+ cleanupModelLoading = setupModelLoading(
156
+ viewer,
157
+ urdfPath,
158
+ packageRef.current,
159
+ setCustomUrdfPath,
160
+ alternativeUrdfModels
161
+ );
162
+ }
163
+
164
+ // Setup joint highlighting
165
+ const cleanupJointHighlighting = setupJointHighlighting(
166
+ viewer,
167
+ setHighlightedJoint
168
+ );
169
+
170
+ // Function to fit the robot to the camera view
171
+ const fitRobotToView = (viewer: URDFViewerElement) => {
172
+ if (!viewer || !viewer.robot) {
173
+ console.log(
174
+ "[RobotViewer] Cannot fit to view: No viewer or robot available"
175
+ );
176
+ return;
177
+ }
178
+
179
+ try {
180
+ // Create a bounding box for the robot
181
+ const boundingBox = new THREE.Box3().setFromObject(viewer.robot);
182
+
183
+ // Calculate the center of the bounding box
184
+ const center = new THREE.Vector3();
185
+ boundingBox.getCenter(center);
186
+
187
+ // Calculate the size of the bounding box
188
+ const size = new THREE.Vector3();
189
+ boundingBox.getSize(size);
190
+
191
+ // Get the maximum dimension to ensure the entire robot is visible
192
+ const maxDim = Math.max(size.x, size.y, size.z);
193
+
194
+ // Position camera to see the center of the model
195
+ viewer.camera.position.copy(center);
196
+
197
+ // Move the camera back to see the entire robot
198
+ // Use the model's up direction to determine which axis to move along
199
+ const upVector = new THREE.Vector3();
200
+ if (viewer.up === "+Z" || viewer.up === "Z") {
201
+ upVector.set(1, 1, 1); // Move back in a diagonal
202
+ } else if (viewer.up === "+Y" || viewer.up === "Y") {
203
+ upVector.set(1, 1, 1); // Move back in a diagonal
204
+ } else {
205
+ upVector.set(1, 1, 1); // Default direction
206
+ }
207
+
208
+ // Normalize the vector and multiply by the size
209
+ upVector.normalize().multiplyScalar(maxDim * 1.3);
210
+ viewer.camera.position.add(upVector);
211
+
212
+ // Make the camera look at the center of the model
213
+ viewer.controls.target.copy(center);
214
+
215
+ // Update controls and mark for redraw
216
+ viewer.controls.update();
217
+ viewer.redraw();
218
+
219
+ console.log("[RobotViewer] Robot auto-fitted to view");
220
+ } catch (error) {
221
+ console.error("[RobotViewer] Error fitting robot to view:", error);
222
+ }
223
+ };
224
+
225
+ // Add event listener for when the robot is loaded to auto-fit to view
226
+ const onRobotLoad = () => {
227
+ fitRobotToView(viewer);
228
+ };
229
+
230
+ // Setup animation event handler for the default model or when hasAnimation is true
231
+ const onModelProcessed = () => {
232
+ hasInitializedRef.current = true;
233
+ if ("setJointValue" in viewer) {
234
+ // Clear any existing animation
235
+ if (cleanupAnimationRef.current) {
236
+ cleanupAnimationRef.current();
237
+ cleanupAnimationRef.current = null;
238
+ }
239
+ }
240
+ // Auto-fit the robot to view when the model is processed
241
+ onRobotLoad();
242
+ };
243
+
244
+ viewer.addEventListener("urdf-processed", onModelProcessed);
245
+
246
+ // Return cleanup function
247
+ return () => {
248
+ if (cleanupAnimationRef.current) {
249
+ cleanupAnimationRef.current();
250
+ cleanupAnimationRef.current = null;
251
+ }
252
+ hasInitializedRef.current = false;
253
+ cleanupJointHighlighting();
254
+ cleanupModelLoading();
255
+ viewer.removeEventListener("urdf-processed", onModelProcessed);
256
+ };
257
+ }, [
258
+ isDefaultModel,
259
+ customUrdfPath,
260
+ urlModifierFunc,
261
+ defaultUrlModifier,
262
+ alternativeUrdfModels,
263
+ ]);
264
+
265
+ return (
266
+ <div
267
+ className={cn(
268
+ "w-full h-full transition-all duration-300 ease-in-out relative",
269
+ "bg-gradient-to-br from-gray-900 to-gray-800"
270
+ )}
271
+ >
272
+ <div ref={containerRef} className="w-full h-full" />
273
+
274
+ {/* Joint highlight indicator */}
275
+ {highlightedJoint && (
276
+ <div className="absolute bottom-4 right-4 bg-black/70 text-white px-3 py-2 rounded-md text-sm font-mono z-10">
277
+ Joint: {highlightedJoint}
278
+ </div>
279
+ )}
280
+
281
+ {/* WebSocket connection status */}
282
+ {isDefaultModel && (
283
+ <div className="absolute top-4 right-4 z-10">
284
+ <div
285
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono ${
286
+ isWebSocketConnected
287
+ ? "bg-green-900/70 text-green-300"
288
+ : "bg-red-900/70 text-red-300"
289
+ }`}
290
+ >
291
+ <div
292
+ className={`w-2 h-2 rounded-full ${
293
+ isWebSocketConnected ? "bg-green-400" : "bg-red-400"
294
+ }`}
295
+ />
296
+ {isWebSocketConnected ? "Live Robot Data" : "Disconnected"}
297
+ </div>
298
+ </div>
299
+ )}
300
+ </div>
301
+ );
302
+ };
303
+
304
+ export default memo(UrdfViewer);
src/components/control/CameraFeed.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { VideoOff } from "lucide-react";
3
+ import { useCameraStream } from "@/hooks/useCameraStream";
4
+
5
+ interface CameraFeedProps {
6
+ /** Browser deviceId to stream. Empty string renders the "no camera" state. */
7
+ deviceId: string;
8
+ /** Optional caption shown under the feed. */
9
+ label?: string;
10
+ }
11
+
12
+ /** Live browser-camera feed bound to a deviceId via getUserMedia. */
13
+ const CameraFeed: React.FC<CameraFeedProps> = ({ deviceId, label }) => {
14
+ const { videoRef, hasError } = useCameraStream(deviceId, false);
15
+ const showVideo = deviceId && !hasError;
16
+
17
+ return (
18
+ <div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
19
+ <div className="aspect-[4/3] bg-gray-800 relative">
20
+ {showVideo ? (
21
+ <video
22
+ ref={videoRef}
23
+ autoPlay
24
+ muted
25
+ playsInline
26
+ className="w-full h-full object-cover"
27
+ />
28
+ ) : (
29
+ <div className="w-full h-full flex flex-col items-center justify-center">
30
+ <VideoOff className="w-8 h-8 text-gray-500 mb-2" />
31
+ <span className="text-gray-500 text-sm">
32
+ {deviceId ? "Preview failed" : "No camera selected"}
33
+ </span>
34
+ </div>
35
+ )}
36
+ </div>
37
+ {label && (
38
+ <div className="p-2 text-sm text-gray-300 truncate border-t border-gray-800">
39
+ {label}
40
+ </div>
41
+ )}
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default CameraFeed;
src/components/control/CommandBar.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { Mic, MicOff, Send, Camera } from 'lucide-react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Input } from '@/components/ui/input';
6
+
7
+ interface CommandBarProps {
8
+ command: string;
9
+ setCommand: (command: string) => void;
10
+ handleSendCommand: () => void;
11
+ isVoiceActive: boolean;
12
+ setIsVoiceActive: (isActive: boolean) => void;
13
+ showCamera: boolean;
14
+ setShowCamera: (show: boolean) => void;
15
+ handleEndSession: () => void;
16
+ }
17
+
18
+ const CommandBar: React.FC<CommandBarProps> = ({
19
+ command,
20
+ setCommand,
21
+ handleSendCommand,
22
+ isVoiceActive,
23
+ setIsVoiceActive,
24
+ showCamera,
25
+ setShowCamera,
26
+ handleEndSession
27
+ }) => {
28
+ return (
29
+ <div className="bg-gray-900 p-4 space-y-4">
30
+ <div className="flex flex-col sm:flex-row gap-4 items-center max-w-4xl mx-auto w-full">
31
+ <Input
32
+ value={command}
33
+ onChange={(e) => setCommand(e.target.value)}
34
+ placeholder="Tell the robot what to do..."
35
+ className="flex-1 bg-gray-800 border-gray-600 text-white placeholder-gray-400 text-lg py-3"
36
+ onKeyPress={(e) => e.key === 'Enter' && handleSendCommand()}
37
+ />
38
+ <Button
39
+ onClick={handleSendCommand}
40
+ className="bg-orange-500 hover:bg-orange-600 px-6 py-3 self-stretch sm:self-auto"
41
+ >
42
+ <Send strokeWidth={1.5} />
43
+ Send
44
+ </Button>
45
+ </div>
46
+
47
+ <div className="flex justify-center items-center gap-6">
48
+ <div className="flex flex-wrap justify-center gap-2 sm:gap-4">
49
+ <Button
50
+ onClick={() => setIsVoiceActive(!isVoiceActive)}
51
+ className={`px-6 py-2 ${
52
+ isVoiceActive ? 'bg-gray-600 text-white hover:bg-gray-500' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
53
+ }`}
54
+ >
55
+ {isVoiceActive ? <Mic strokeWidth={1.5} /> : <MicOff strokeWidth={1.5} />}
56
+ Voice Command
57
+ </Button>
58
+
59
+ <Button
60
+ onClick={() => setShowCamera(!showCamera)}
61
+ className={`px-6 py-2 ${
62
+ showCamera ? 'bg-gray-600 text-white hover:bg-gray-500' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
63
+ }`}
64
+ >
65
+ <Camera strokeWidth={1.5} />
66
+ Show Camera
67
+ </Button>
68
+
69
+ <Button
70
+ onClick={handleEndSession}
71
+ className="bg-red-600 hover:bg-red-700 px-6 py-2"
72
+ >
73
+ End Session
74
+ </Button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default CommandBar;
src/components/control/MetricsPanel.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect, useRef } from 'react';
3
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
4
+ import { Camera, MicOff } from 'lucide-react';
5
+
6
+ interface MetricsPanelProps {
7
+ activeTab: 'SENSORS' | 'MOTORS';
8
+ setActiveTab: (tab: 'SENSORS' | 'MOTORS') => void;
9
+ sensorData: any[];
10
+ motorData: any[];
11
+ hasPermissions: boolean;
12
+ streamRef: React.RefObject<MediaStream | null>;
13
+ isVoiceActive: boolean;
14
+ micLevel: number;
15
+ }
16
+
17
+ const MetricsPanel: React.FC<MetricsPanelProps> = ({
18
+ activeTab,
19
+ setActiveTab,
20
+ sensorData,
21
+ motorData,
22
+ hasPermissions,
23
+ streamRef,
24
+ isVoiceActive,
25
+ micLevel,
26
+ }) => {
27
+ const sensorVideoRef = useRef<HTMLVideoElement>(null);
28
+
29
+ useEffect(() => {
30
+ if (activeTab === 'SENSORS' && hasPermissions && sensorVideoRef.current && streamRef.current) {
31
+ if (sensorVideoRef.current.srcObject !== streamRef.current) {
32
+ sensorVideoRef.current.srcObject = streamRef.current;
33
+ }
34
+ }
35
+ }, [activeTab, hasPermissions, streamRef]);
36
+
37
+ return (
38
+ <div className="w-full lg:w-1/2 p-2 sm:p-4">
39
+ <div className="bg-gray-900 rounded-lg p-4 h-full flex flex-col">
40
+ {/* Tab Headers */}
41
+ <div className="flex mb-4">
42
+ <button
43
+ onClick={() => setActiveTab('MOTORS')}
44
+ className={`px-6 py-2 rounded-t-lg text-sm sm:text-base ${
45
+ activeTab === 'MOTORS'
46
+ ? 'bg-orange-500 text-white'
47
+ : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
48
+ }`}
49
+ >
50
+ MOTORS
51
+ </button>
52
+ <button
53
+ onClick={() => setActiveTab('SENSORS')}
54
+ className={`px-6 py-2 rounded-t-lg ml-2 text-sm sm:text-base ${
55
+ activeTab === 'SENSORS'
56
+ ? 'bg-orange-500 text-white'
57
+ : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
58
+ }`}
59
+ >
60
+ SENSORS
61
+ </button>
62
+ </div>
63
+
64
+ {/* Chart Content */}
65
+ <div className="flex-1 overflow-y-auto">
66
+ {activeTab === 'SENSORS' && (
67
+ <div className="space-y-4">
68
+ {/* Webcam Feed */}
69
+ <div className="border border-gray-800 rounded p-2 flex flex-col h-64">
70
+ <h3 className="text-sm text-white font-medium mb-2">Live Camera Feed</h3>
71
+ {hasPermissions ? (
72
+ <div className="flex-1 bg-black rounded overflow-hidden">
73
+ <video
74
+ ref={sensorVideoRef}
75
+ autoPlay
76
+ muted
77
+ playsInline
78
+ className="w-full h-full object-contain"
79
+ />
80
+ </div>
81
+ ) : (
82
+ <div className="flex-1 flex items-center justify-center bg-black rounded">
83
+ <div className="text-center">
84
+ <Camera className="w-12 h-12 mx-auto text-gray-500 mb-2" />
85
+ <p className="text-gray-400">Camera permission not granted.</p>
86
+ </div>
87
+ </div>
88
+ )}
89
+ </div>
90
+
91
+ {/* Mic Detection & Other Sensors */}
92
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
93
+ <div className="border border-gray-800 rounded p-2 flex flex-col justify-center min-h-[120px]">
94
+ <h3 className="text-sm text-center text-white font-medium mb-2">Voice Activity</h3>
95
+ {hasPermissions ? (
96
+ <div className="flex-1 flex flex-col items-center justify-center gap-2 text-center">
97
+ <div className="flex items-end h-10 gap-px w-full justify-center">
98
+ {[...Array(15)].map((_, i) => {
99
+ const barIsActive = isVoiceActive && i < (micLevel / 120 * 15);
100
+ return (
101
+ <div
102
+ key={i}
103
+ className={`w-1.5 rounded-full transition-colors duration-75 ${barIsActive ? 'bg-orange-500' : 'bg-gray-700'}`}
104
+ style={{ height: `${(i / 15 * 60) + 20}%` }}
105
+ />
106
+ );
107
+ })}
108
+ </div>
109
+ <p className="text-xs text-gray-300">
110
+ {isVoiceActive ? "Voice commands active" : "Voice commands muted"}
111
+ </p>
112
+ </div>
113
+ ) : (
114
+ <div className="flex-1 flex items-center justify-center bg-black rounded">
115
+ <div className="text-center">
116
+ <MicOff className="w-8 h-8 mx-auto text-gray-500 mb-2" />
117
+ <p className="text-gray-400">Microphone permission not granted.</p>
118
+ </div>
119
+ </div>
120
+ )}
121
+ </div>
122
+
123
+ {/* Sensor Charts */}
124
+ {['sensor3', 'sensor4'].map((sensor, index) => (
125
+ <div key={sensor} className="border border-gray-800 rounded p-2 flex flex-col h-auto min-h-[120px]">
126
+ <h3 className="text-sm text-white font-medium mb-2">Sensor {index + 3}</h3>
127
+ <ResponsiveContainer width="100%" height="90%">
128
+ <LineChart data={sensorData}>
129
+ <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
130
+ <XAxis hide />
131
+ <YAxis fontSize={12} stroke="#9CA3AF" />
132
+ <Tooltip
133
+ contentStyle={{
134
+ backgroundColor: '#1F2937',
135
+ border: '1px solid #374151',
136
+ color: '#fff'
137
+ }}
138
+ />
139
+ <Line
140
+ type="monotone"
141
+ dataKey={sensor}
142
+ stroke={index % 2 === 1 ? '#ff6b35' : '#ffdd44'}
143
+ strokeWidth={2}
144
+ dot={false}
145
+ />
146
+ </LineChart>
147
+ </ResponsiveContainer>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ </div>
152
+ )}
153
+
154
+ {activeTab === 'MOTORS' && (
155
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
156
+ {['motor1', 'motor2', 'motor3', 'motor4', 'motor5', 'motor6'].map((motor, index) => (
157
+ <div key={motor} className="border border-gray-800 rounded p-2 h-40">
158
+ <h3 className="text-sm text-white font-medium mb-2">Motor {index + 1}</h3>
159
+ <ResponsiveContainer width="100%" height="80%">
160
+ <LineChart data={motorData}>
161
+ <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
162
+ <XAxis hide />
163
+ <YAxis fontSize={12} stroke="#9CA3AF" />
164
+ <Tooltip
165
+ contentStyle={{
166
+ backgroundColor: '#1F2937',
167
+ border: '1px solid #374151',
168
+ color: '#fff'
169
+ }}
170
+ />
171
+ <Line
172
+ type="monotone"
173
+ dataKey={motor}
174
+ stroke={index % 2 === 0 ? '#ff6b35' : '#ffdd44'}
175
+ strokeWidth={2}
176
+ dot={false}
177
+ />
178
+ </LineChart>
179
+ </ResponsiveContainer>
180
+ </div>
181
+ ))}
182
+ </div>
183
+ )}
184
+ </div>
185
+ </div>
186
+ </div>
187
+ );
188
+ };
189
+
190
+ export default MetricsPanel;
src/components/control/TeleopCameraPanel.tsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { RefreshCw } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Switch } from "@/components/ui/switch";
6
+ import { useRobots } from "@/hooks/useRobots";
7
+ import CameraFeed from "./CameraFeed";
8
+
9
+ /**
10
+ * Optional live camera panel for the teleoperation page. Off by default so we
11
+ * never call getUserMedia just by landing on the page (same consent pattern as
12
+ * the calibration camera toggle). Teleoperation opens no cv2 cameras, so the
13
+ * browser can stream them directly while the arm runs.
14
+ *
15
+ * A strict mirror of the selected robot's configured cameras: one live feed per
16
+ * camera on the robot record (e.g. "wrist_cam", "webcam"), stacked vertically.
17
+ * If the robot has none configured it shows nothing — teleop never surfaces a
18
+ * device that wasn't deliberately added to the robot.
19
+ */
20
+ const TeleopCameraPanel: React.FC = () => {
21
+ const [enabled, setEnabled] = useState(false);
22
+ // Bumped by the retry button to remount the feeds (a fresh getUserMedia
23
+ // attempt) — useful if a camera was unplugged and reconnected.
24
+ const [reloadKey, setReloadKey] = useState(0);
25
+ const { selectedRecord, isLoading: robotsLoading } = useRobots();
26
+
27
+ // Feeds come solely from the robot's configured cameras; each carries a stored
28
+ // browser device_id we stream directly. A configured camera whose device is
29
+ // currently absent still shows (name + failed-preview placeholder), so the
30
+ // user can tell it's expected but not detected.
31
+ const configured = selectedRecord?.cameras ?? [];
32
+ const feeds = configured.map((c) => ({
33
+ key: c.id,
34
+ name: c.name,
35
+ deviceId: c.device_id,
36
+ }));
37
+
38
+ return (
39
+ <div className="bg-gray-900 rounded-lg p-4 flex flex-col gap-4 h-full">
40
+ <div className="flex items-center justify-between">
41
+ <h2 className="text-xl font-medium text-gray-200">Cameras</h2>
42
+ <div className="flex items-center gap-2">
43
+ {enabled && feeds.length > 0 && (
44
+ <Button
45
+ type="button"
46
+ variant="ghost"
47
+ size="icon"
48
+ onClick={() => setReloadKey((k) => k + 1)}
49
+ className="h-9 w-9 text-gray-400 hover:text-white flex-shrink-0"
50
+ title="Retry camera feeds (e.g. after reconnecting a camera)"
51
+ aria-label="Retry camera feeds"
52
+ >
53
+ <RefreshCw className="w-4 h-4" />
54
+ </Button>
55
+ )}
56
+ <Label htmlFor="teleop-camera-toggle" className="text-sm text-gray-400">
57
+ {enabled ? "On" : "Off"}
58
+ </Label>
59
+ <Switch
60
+ id="teleop-camera-toggle"
61
+ checked={enabled}
62
+ onCheckedChange={setEnabled}
63
+ />
64
+ </div>
65
+ </div>
66
+
67
+ {enabled ? (
68
+ feeds.length > 0 ? (
69
+ <div className="flex flex-col gap-3 overflow-y-auto">
70
+ {feeds.map((feed) => (
71
+ <CameraFeed
72
+ key={`${feed.key}:${reloadKey}`}
73
+ deviceId={feed.deviceId}
74
+ label={feed.name}
75
+ />
76
+ ))}
77
+ </div>
78
+ ) : (
79
+ <p className="text-sm text-gray-500">
80
+ {robotsLoading
81
+ ? "Loading robot..."
82
+ : "No cameras configured for this robot. Add them during calibration to see live feeds here."}
83
+ </p>
84
+ )
85
+ ) : (
86
+ <p className="text-sm text-gray-500">
87
+ Turn on to watch your cameras while you teleoperate.
88
+ </p>
89
+ )}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export default TeleopCameraPanel;
src/components/control/VisualizerPanel.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { ArrowLeft } from "lucide-react";
4
+ import { cn } from "@/lib/utils";
5
+ import UrdfViewer from "../UrdfViewer";
6
+ import Logo from "@/components/Logo";
7
+
8
+ interface VisualizerPanelProps {
9
+ onGoBack: () => void;
10
+ className?: string;
11
+ /** Optional content rendered as a column beside the 3D viewer (e.g. a camera panel). */
12
+ rightSlot?: React.ReactNode;
13
+ }
14
+
15
+ const VisualizerPanel: React.FC<VisualizerPanelProps> = ({
16
+ onGoBack,
17
+ className,
18
+ rightSlot,
19
+ }) => {
20
+ return (
21
+ <div
22
+ className={cn(
23
+ "w-full p-2 sm:p-4 space-y-4 lg:space-y-0 lg:space-x-4 flex flex-col lg:flex-row",
24
+ className
25
+ )}
26
+ >
27
+ <div className="bg-gray-900 rounded-lg p-4 flex-1 flex flex-col">
28
+ <div className="flex items-center gap-4 mb-4">
29
+ <Button
30
+ variant="ghost"
31
+ size="icon"
32
+ onClick={onGoBack}
33
+ className="text-gray-400 hover:text-white hover:bg-gray-800 flex-shrink-0"
34
+ >
35
+ <ArrowLeft className="h-5 w-5" />
36
+ </Button>
37
+ <Logo iconOnly={true} />
38
+ <div className="w-px h-6 bg-gray-700" />
39
+ <h2 className="text-xl font-medium text-gray-200">Teleoperation</h2>
40
+ </div>
41
+ <div className="flex-1 bg-black rounded border border-gray-800 min-h-[50vh] lg:min-h-0">
42
+ <UrdfViewer />
43
+ </div>
44
+ </div>
45
+ {rightSlot && (
46
+ <div className="lg:w-96 flex flex-col">{rightSlot}</div>
47
+ )}
48
+ </div>
49
+ );
50
+ };
51
+
52
+ export default VisualizerPanel;
src/components/jobs/CheckpointDropdown.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "@/components/ui/select";
9
+ import { JobCheckpoint } from "@/lib/checkpointsApi";
10
+
11
+ interface Props {
12
+ checkpoints: JobCheckpoint[];
13
+ selectedStep: number | null;
14
+ onChange: (step: number) => void;
15
+ disabled?: boolean;
16
+ placeholder?: string;
17
+ }
18
+
19
+ export const CheckpointDropdown: React.FC<Props> = ({
20
+ checkpoints,
21
+ selectedStep,
22
+ onChange,
23
+ disabled,
24
+ placeholder = "Select checkpoint",
25
+ }) => {
26
+ // step 0 is the sentinel for an imported single-model checkpoint (lerobot
27
+ // never saves at step 0), so it has no meaningful step number — show
28
+ // "latest" instead. Real training checkpoints keep their step label.
29
+ const labelFor = (step: number) => (step === 0 ? "latest" : `step ${step}`);
30
+ const value = selectedStep != null ? String(selectedStep) : undefined;
31
+ return (
32
+ <Select
33
+ value={value}
34
+ onValueChange={(v) => onChange(Number(v))}
35
+ disabled={disabled || checkpoints.length === 0}
36
+ >
37
+ <SelectTrigger
38
+ className="bg-slate-800 border-slate-700 text-white h-8 text-xs px-2 w-auto min-w-[110px]"
39
+ onClick={(e) => e.stopPropagation()}
40
+ >
41
+ <SelectValue placeholder={placeholder} />
42
+ </SelectTrigger>
43
+ <SelectContent className="bg-slate-900 border-slate-700 text-white">
44
+ {checkpoints.map((c) => (
45
+ <SelectItem
46
+ key={c.step}
47
+ value={String(c.step)}
48
+ onClick={(e) => e.stopPropagation()}
49
+ >
50
+ {labelFor(c.step)}
51
+ </SelectItem>
52
+ ))}
53
+ </SelectContent>
54
+ </Select>
55
+ );
56
+ };
57
+
58
+ export default CheckpointDropdown;