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

Sync from leLab @ 84b6251cec3471195dfa35119827fbd1da2a801e

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 +42 -0
  2. .gitignore +49 -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 +91 -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 +68 -0
  39. src/components/Footer.tsx +68 -0
  40. src/components/Logo.tsx +19 -0
  41. src/components/SingleTabGuard.tsx +141 -0
  42. src/components/TeleopStopNotice.tsx +36 -0
  43. src/components/UpdateNotice.tsx +194 -0
  44. src/components/UrdfViewer.tsx +310 -0
  45. src/components/control/BrowserCameraPreview.test.tsx +51 -0
  46. src/components/control/BrowserCameraPreview.tsx +75 -0
  47. src/components/control/CameraFeed.tsx +22 -0
  48. src/components/control/CameraRotationSelect.tsx +30 -0
  49. src/components/control/CommandBar.tsx +81 -0
  50. src/components/control/MetricsPanel.tsx +190 -0
.gitattributes ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Plain binary (overrides LFS): installing from git skips LFS, and the build copies these into the wheel.
40
+ # sync_space.yml puts them back in LFS for the Space.
41
+ public/so-101-urdf/meshes/*.stl -filter -diff -merge
42
+ public/so-101-urdf/meshes/*.stl filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ /node_modules
3
+ /.pnp
4
+ .pnp.js
5
+
6
+ # Testing
7
+ /coverage
8
+
9
+ # Production
10
+ /build
11
+ /dist
12
+
13
+ # Misc
14
+ .DS_Store
15
+ .env
16
+ .env.local
17
+ .env.development.local
18
+ .env.test.local
19
+ .env.production.local
20
+ .env*.local
21
+
22
+ # Debug logs
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ *.swo
32
+
33
+ # TypeScript
34
+ *.tsbuildinfo
35
+
36
+ # Optional npm cache directory
37
+ .npm
38
+
39
+ # Optional eslint cache
40
+ .eslintcache
41
+
42
+ # Optional REPL history
43
+ .node_repl_history
44
+
45
+ # Output of 'npm pack'
46
+ *.tgz
47
+
48
+ # Yarn Integrity file
49
+ .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,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vite_react_shadcn_ts",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "vitest run",
8
+ "dev": "vite",
9
+ "build": "vite build",
10
+ "build:dev": "vite build --mode development",
11
+ "lint": "eslint .",
12
+ "preview": "vite preview"
13
+ },
14
+ "dependencies": {
15
+ "@hookform/resolvers": "^3.9.0",
16
+ "@radix-ui/react-accordion": "^1.2.0",
17
+ "@radix-ui/react-alert-dialog": "^1.1.1",
18
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
19
+ "@radix-ui/react-avatar": "^1.1.0",
20
+ "@radix-ui/react-checkbox": "^1.1.1",
21
+ "@radix-ui/react-collapsible": "^1.1.0",
22
+ "@radix-ui/react-context-menu": "^2.2.1",
23
+ "@radix-ui/react-dialog": "^1.1.2",
24
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
25
+ "@radix-ui/react-hover-card": "^1.1.1",
26
+ "@radix-ui/react-label": "^2.1.0",
27
+ "@radix-ui/react-menubar": "^1.1.1",
28
+ "@radix-ui/react-navigation-menu": "^1.2.0",
29
+ "@radix-ui/react-popover": "^1.1.1",
30
+ "@radix-ui/react-progress": "^1.1.0",
31
+ "@radix-ui/react-radio-group": "^1.2.0",
32
+ "@radix-ui/react-scroll-area": "^1.1.0",
33
+ "@radix-ui/react-select": "^2.1.1",
34
+ "@radix-ui/react-separator": "^1.1.0",
35
+ "@radix-ui/react-slider": "^1.2.0",
36
+ "@radix-ui/react-slot": "^1.1.0",
37
+ "@radix-ui/react-switch": "^1.1.0",
38
+ "@radix-ui/react-tabs": "^1.1.0",
39
+ "@radix-ui/react-toast": "^1.2.1",
40
+ "@radix-ui/react-toggle": "^1.1.0",
41
+ "@radix-ui/react-toggle-group": "^1.1.0",
42
+ "@radix-ui/react-tooltip": "^1.1.4",
43
+ "@react-three/drei": "^9.122.0",
44
+ "@react-three/fiber": "^8.18.0",
45
+ "@tanstack/react-query": "^5.56.2",
46
+ "class-variance-authority": "^0.7.1",
47
+ "clsx": "^2.1.1",
48
+ "cmdk": "^1.0.0",
49
+ "date-fns": "^3.6.0",
50
+ "embla-carousel-react": "^8.3.0",
51
+ "input-otp": "^1.2.4",
52
+ "jszip": "^3.10.1",
53
+ "lucide-react": "^0.462.0",
54
+ "next-themes": "^0.3.0",
55
+ "react": "^18.3.1",
56
+ "react-day-picker": "^8.10.1",
57
+ "react-dom": "^18.3.1",
58
+ "react-hook-form": "^7.53.0",
59
+ "react-resizable-panels": "^2.1.3",
60
+ "react-router-dom": "^7.18.3",
61
+ "recharts": "^2.12.7",
62
+ "sonner": "^1.5.0",
63
+ "tailwind-merge": "^2.5.2",
64
+ "tailwindcss-animate": "^1.0.7",
65
+ "three": "^0.177.0",
66
+ "urdf-loader": "^0.12.6",
67
+ "vaul": "^0.9.3",
68
+ "zod": "^3.23.8"
69
+ },
70
+ "devDependencies": {
71
+ "@eslint/js": "^9.9.0",
72
+ "@tailwindcss/typography": "^0.5.15",
73
+ "@types/node": "^22.5.5",
74
+ "@types/react": "^18.3.3",
75
+ "@types/react-dom": "^18.3.0",
76
+ "@vitejs/plugin-react-swc": "^4.3.1",
77
+ "autoprefixer": "^10.4.20",
78
+ "eslint": "^9.9.0",
79
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
80
+ "eslint-plugin-react-refresh": "^0.4.9",
81
+ "globals": "^15.9.0",
82
+ "jsdom": "^28.1.0",
83
+ "lovable-tagger": "^1.3.0",
84
+ "postcss": "^8.5.25",
85
+ "tailwindcss": "^3.4.11",
86
+ "typescript": "^5.5.3",
87
+ "typescript-eslint": "^8.0.1",
88
+ "vite": "^8.0.16",
89
+ "vitest": "^5.0.1"
90
+ }
91
+ }
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,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { OnboardingProvider } from "./contexts/OnboardingContext";
24
+
25
+ const queryClient = new QueryClient();
26
+
27
+ function App() {
28
+ return (
29
+ <QueryClientProvider client={queryClient}>
30
+ <TooltipProvider>
31
+ <ThemeProvider>
32
+ <ApiProvider>
33
+ <HfAuthProvider>
34
+ <UrdfProvider>
35
+ <DragAndDropProvider>
36
+ <BrowserRouter>
37
+ <SingleTabGuard>
38
+ <OnboardingProvider>
39
+ <TeleopStopNotice />
40
+ <UpdateNotice />
41
+ <Routes>
42
+ <Route path="/" element={<Landing />} />
43
+ <Route path="/teleoperation" element={<Teleoperation />} />
44
+ <Route path="/recording" element={<Recording />} />
45
+ <Route path="/upload" element={<Upload />} />
46
+ <Route path="/training" element={<Training />} />
47
+ <Route path="/training/:jobId" element={<Training />} />
48
+ <Route path="/inference" element={<Inference />} />
49
+ <Route path="/calibration" element={<Calibration />} />
50
+ <Route path="/edit-dataset" element={<EditDataset />} />
51
+
52
+ <Route path="*" element={<NotFound />} />
53
+ </Routes>
54
+ </OnboardingProvider>
55
+ </SingleTabGuard>
56
+ <Toaster />
57
+ </BrowserRouter>
58
+ </DragAndDropProvider>
59
+ </UrdfProvider>
60
+ </HfAuthProvider>
61
+ </ApiProvider>
62
+ </ThemeProvider>
63
+ </TooltipProvider>
64
+ </QueryClientProvider>
65
+ );
66
+ }
67
+
68
+ 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,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // A non-primary tab renders only the blocker, not `children`. Anything the
112
+ // app mounts as a descendant (a Radix modal that sets `pointer-events: none`
113
+ // on `body`, the onboarding welcome dialog and `?` launcher) would otherwise
114
+ // sit behind this overlay and, in the modal case, kill the "Use this tab"
115
+ // button along with the rest of the page.
116
+ return (
117
+ <>
118
+ {isPrimary && children}
119
+ {!isPrimary && (
120
+ <div
121
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm"
122
+ role="dialog"
123
+ aria-modal="true"
124
+ >
125
+ <div className="mx-4 max-w-md space-y-4 rounded-lg border bg-background p-6 text-center shadow-lg">
126
+ <h2 className="text-lg font-semibold">
127
+ LeLab is already open in another tab
128
+ </h2>
129
+ <p className="text-sm text-muted-foreground">
130
+ Only one tab can control the robot at a time. Switch back to the
131
+ original tab, or take over here — the other tab will lock.
132
+ </p>
133
+ <Button onClick={takeOver}>Use this tab</Button>
134
+ </div>
135
+ </div>
136
+ )}
137
+ </>
138
+ );
139
+ };
140
+
141
+ 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,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ fitGridToRobot,
17
+ setupMeshLoader,
18
+ setupJointHighlighting,
19
+ setupModelLoading,
20
+ URDFViewerElement,
21
+ } from "@/lib/urdfViewerHelpers";
22
+
23
+ // Register the URDFManipulator as a custom element if it hasn't been already
24
+ if (typeof window !== "undefined" && !customElements.get("urdf-viewer")) {
25
+ customElements.define("urdf-viewer", URDFManipulator);
26
+ }
27
+ import * as THREE from "three";
28
+
29
+ // Extend the interface for the URDF viewer element to include background property
30
+ interface UrdfViewerElement extends HTMLElement {
31
+ background?: string;
32
+ setJointValue?: (jointName: string, value: number) => void;
33
+ }
34
+
35
+ const UrdfViewer: React.FC = () => {
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+ const [highlightedJoint, setHighlightedJoint] = useState<string | null>(null);
38
+ const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel } =
39
+ useUrdf();
40
+
41
+ const cleanupAnimationRef = useRef<(() => void) | null>(null);
42
+ const viewerRef = useRef<URDFViewerElement | null>(null);
43
+ const hasInitializedRef = useRef<boolean>(false);
44
+
45
+ // Real-time joint updates via WebSocket
46
+ const { isConnected: isWebSocketConnected } = useRealTimeJoints({
47
+ viewerRef,
48
+ enabled: isDefaultModel, // Only enable WebSocket for default model
49
+ });
50
+
51
+ // Add state for custom URDF path
52
+ const [customUrdfPath, setCustomUrdfPath] = useState<string | null>(null);
53
+ const [urlModifierFunc, setUrlModifierFunc] = useState<
54
+ ((url: string) => string) | null
55
+ >(null);
56
+
57
+ const packageRef = useRef<string>("");
58
+
59
+ // Implement UrdfProcessor interface for drag and drop
60
+ const urdfProcessor = useMemo(
61
+ () => ({
62
+ loadUrdf: (urdfPath: string) => {
63
+ setCustomUrdfPath(urdfPath);
64
+ },
65
+ setUrlModifierFunc: (func: (url: string) => string) => {
66
+ setUrlModifierFunc(() => func);
67
+ },
68
+ getPackage: () => {
69
+ return packageRef.current;
70
+ },
71
+ }),
72
+ []
73
+ );
74
+
75
+ // Register the URDF processor with the global drag and drop context
76
+ useEffect(() => {
77
+ registerUrdfProcessor(urdfProcessor);
78
+ }, [registerUrdfProcessor, urdfProcessor]);
79
+
80
+ // Create URL modifier function for default model
81
+ const defaultUrlModifier = useCallback((url: string) => {
82
+ console.log(`🔗 defaultUrlModifier called with: ${url}`);
83
+
84
+ // Handle various package:// URL formats for the default SO-101 model
85
+ if (url.startsWith("package://so_arm_description/meshes/")) {
86
+ const modifiedUrl = url.replace(
87
+ "package://so_arm_description/meshes/",
88
+ "/so-101-urdf/meshes/"
89
+ );
90
+ console.log(`🔗 Modified URL (package): ${modifiedUrl}`);
91
+ return modifiedUrl;
92
+ }
93
+
94
+ // Handle case where package path might be partially resolved
95
+ if (url.includes("so_arm_description/meshes/")) {
96
+ const modifiedUrl = url.replace(
97
+ /.*so_arm_description\/meshes\//,
98
+ "/so-101-urdf/meshes/"
99
+ );
100
+ console.log(`🔗 Modified URL (partial): ${modifiedUrl}`);
101
+ return modifiedUrl;
102
+ }
103
+
104
+ // Handle the specific problematic path pattern we're seeing in logs
105
+ if (url.includes("/so-101-urdf/so_arm_description/meshes/")) {
106
+ const modifiedUrl = url.replace(
107
+ "/so-101-urdf/so_arm_description/meshes/",
108
+ "/so-101-urdf/meshes/"
109
+ );
110
+ console.log(`🔗 Modified URL (problematic path): ${modifiedUrl}`);
111
+ return modifiedUrl;
112
+ }
113
+
114
+ // Handle relative paths that might need mesh folder prefix
115
+ if (
116
+ url.endsWith(".stl") &&
117
+ !url.startsWith("/") &&
118
+ !url.startsWith("http")
119
+ ) {
120
+ const modifiedUrl = `/so-101-urdf/meshes/${url}`;
121
+ console.log(`🔗 Modified URL (relative): ${modifiedUrl}`);
122
+ return modifiedUrl;
123
+ }
124
+
125
+ console.log(`🔗 Unmodified URL: ${url}`);
126
+ return url;
127
+ }, []);
128
+
129
+ // Main effect to create and setup the viewer only once
130
+ useEffect(() => {
131
+ if (!containerRef.current) return;
132
+
133
+ // Create and configure the URDF viewer element
134
+ const viewer = createUrdfViewer(containerRef.current, true);
135
+ viewerRef.current = viewer; // Store reference to the viewer
136
+
137
+ // Setup mesh loading function with appropriate URL modifier
138
+ const activeUrlModifier = isDefaultModel
139
+ ? defaultUrlModifier
140
+ : urlModifierFunc;
141
+ setupMeshLoader(viewer, activeUrlModifier);
142
+
143
+ // Determine which URDF to load - fixed path to match the actual available file
144
+ const urdfPath = isDefaultModel
145
+ ? "/so-101-urdf/urdf/so101_new_calib.urdf"
146
+ : customUrdfPath || "";
147
+
148
+ // Set the package path for the default model
149
+ if (isDefaultModel) {
150
+ packageRef.current = "/"; // Set to root so we can handle full path resolution in URL modifier
151
+ }
152
+
153
+ // Setup model loading if a path is available
154
+ let cleanupModelLoading = () => {};
155
+ if (urdfPath) {
156
+ cleanupModelLoading = setupModelLoading(
157
+ viewer,
158
+ urdfPath,
159
+ packageRef.current,
160
+ setCustomUrdfPath,
161
+ alternativeUrdfModels
162
+ );
163
+ }
164
+
165
+ // Setup joint highlighting
166
+ const cleanupJointHighlighting = setupJointHighlighting(
167
+ viewer,
168
+ setHighlightedJoint
169
+ );
170
+
171
+ // Function to fit the robot to the camera view
172
+ const fitRobotToView = (viewer: URDFViewerElement) => {
173
+ if (!viewer || !viewer.robot) {
174
+ console.log(
175
+ "[RobotViewer] Cannot fit to view: No viewer or robot available"
176
+ );
177
+ return;
178
+ }
179
+
180
+ try {
181
+ // Create a bounding box for the robot
182
+ const boundingBox = new THREE.Box3().setFromObject(viewer.robot);
183
+
184
+ // Calculate the center of the bounding box
185
+ const center = new THREE.Vector3();
186
+ boundingBox.getCenter(center);
187
+
188
+ // Calculate the size of the bounding box
189
+ const size = new THREE.Vector3();
190
+ boundingBox.getSize(size);
191
+
192
+ // Get the maximum dimension to ensure the entire robot is visible
193
+ const maxDim = Math.max(size.x, size.y, size.z);
194
+
195
+ // Rescale the ground grid to this robot's actual unit scale — it
196
+ // starts as a 1x1 placeholder since it's created before any model
197
+ // is loaded (see createUrdfViewer).
198
+ fitGridToRobot(viewer, center, boundingBox.min.y, maxDim);
199
+
200
+ // Position camera to see the center of the model
201
+ viewer.camera.position.copy(center);
202
+
203
+ // Move the camera back to see the entire robot
204
+ // Use the model's up direction to determine which axis to move along
205
+ const upVector = new THREE.Vector3();
206
+ if (viewer.up === "+Z" || viewer.up === "Z") {
207
+ upVector.set(1, 1, 1); // Move back in a diagonal
208
+ } else if (viewer.up === "+Y" || viewer.up === "Y") {
209
+ upVector.set(1, 1, 1); // Move back in a diagonal
210
+ } else {
211
+ upVector.set(1, 1, 1); // Default direction
212
+ }
213
+
214
+ // Normalize the vector and multiply by the size
215
+ upVector.normalize().multiplyScalar(maxDim * 1.3);
216
+ viewer.camera.position.add(upVector);
217
+
218
+ // Make the camera look at the center of the model
219
+ viewer.controls.target.copy(center);
220
+
221
+ // Update controls and mark for redraw
222
+ viewer.controls.update();
223
+ viewer.redraw();
224
+
225
+ console.log("[RobotViewer] Robot auto-fitted to view");
226
+ } catch (error) {
227
+ console.error("[RobotViewer] Error fitting robot to view:", error);
228
+ }
229
+ };
230
+
231
+ // Add event listener for when the robot is loaded to auto-fit to view
232
+ const onRobotLoad = () => {
233
+ fitRobotToView(viewer);
234
+ };
235
+
236
+ // Setup animation event handler for the default model or when hasAnimation is true
237
+ const onModelProcessed = () => {
238
+ hasInitializedRef.current = true;
239
+ if ("setJointValue" in viewer) {
240
+ // Clear any existing animation
241
+ if (cleanupAnimationRef.current) {
242
+ cleanupAnimationRef.current();
243
+ cleanupAnimationRef.current = null;
244
+ }
245
+ }
246
+ // Auto-fit the robot to view when the model is processed
247
+ onRobotLoad();
248
+ };
249
+
250
+ viewer.addEventListener("urdf-processed", onModelProcessed);
251
+
252
+ // Return cleanup function
253
+ return () => {
254
+ if (cleanupAnimationRef.current) {
255
+ cleanupAnimationRef.current();
256
+ cleanupAnimationRef.current = null;
257
+ }
258
+ hasInitializedRef.current = false;
259
+ cleanupJointHighlighting();
260
+ cleanupModelLoading();
261
+ viewer.removeEventListener("urdf-processed", onModelProcessed);
262
+ };
263
+ }, [
264
+ isDefaultModel,
265
+ customUrdfPath,
266
+ urlModifierFunc,
267
+ defaultUrlModifier,
268
+ alternativeUrdfModels,
269
+ ]);
270
+
271
+ return (
272
+ <div
273
+ className={cn(
274
+ "w-full h-full transition-all duration-300 ease-in-out relative",
275
+ "bg-gradient-to-br from-gray-900 to-gray-800"
276
+ )}
277
+ >
278
+ <div ref={containerRef} className="w-full h-full" />
279
+
280
+ {/* Joint highlight indicator */}
281
+ {highlightedJoint && (
282
+ <div className="absolute bottom-4 right-4 bg-black/70 text-white px-3 py-2 rounded-md text-sm font-mono z-10">
283
+ Joint: {highlightedJoint}
284
+ </div>
285
+ )}
286
+
287
+ {/* WebSocket connection status */}
288
+ {isDefaultModel && (
289
+ <div className="absolute top-4 right-4 z-10">
290
+ <div
291
+ className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono ${
292
+ isWebSocketConnected
293
+ ? "bg-green-900/70 text-green-300"
294
+ : "bg-red-900/70 text-red-300"
295
+ }`}
296
+ >
297
+ <div
298
+ className={`w-2 h-2 rounded-full ${
299
+ isWebSocketConnected ? "bg-green-400" : "bg-red-400"
300
+ }`}
301
+ />
302
+ {isWebSocketConnected ? "Live Robot Data" : "Disconnected"}
303
+ </div>
304
+ </div>
305
+ )}
306
+ </div>
307
+ );
308
+ };
309
+
310
+ export default memo(UrdfViewer);
src/components/control/BrowserCameraPreview.test.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @vitest-environment jsdom
2
+ import React, { act } from "react";
3
+ import { createRoot, Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, expect, test, vi } from "vitest";
5
+ import BrowserCameraPreview from "./BrowserCameraPreview";
6
+ import CameraRotationSelect from "./CameraRotationSelect";
7
+ vi.mock("@/hooks/useCameraStream", () => ({ useCameraStream: () => ({ videoRef: React.createRef<HTMLVideoElement>(), hasError: false }) }));
8
+ let root: Root;
9
+ let container: HTMLDivElement;
10
+ beforeEach(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); container = document.createElement("div"); root = createRoot(container); });
11
+ afterEach(() => act(() => root.unmount()));
12
+ test("the Rotation control exposes four clockwise values and emits a number", () => {
13
+ const change = vi.fn();
14
+ act(() => root.render(<CameraRotationSelect rotation={180} onChange={change} />));
15
+ const select = container.querySelector("select")!;
16
+ expect(container.querySelector("label")!.htmlFor).toBe(select.id);
17
+ expect(select.value).toBe("180");
18
+ expect(Array.from(select.options).map((option) => option.text)).toEqual(["0° clockwise", "90° clockwise", "180° clockwise", "270° clockwise"]);
19
+ act(() => { select.value = "90"; select.dispatchEvent(new Event("change", { bubbles: true })); });
20
+ expect(change).toHaveBeenCalledWith(90);
21
+ });
22
+ test("a quarter turn fills the rotated recorded frame", () => {
23
+ act(() => root.render(<BrowserCameraPreview deviceId="wrist" rotation={90} width={640} height={480} />));
24
+ const video = container.querySelector("video")!, wrapper = video.parentElement!;
25
+ expect(wrapper.style.aspectRatio).toBe("480 / 640");
26
+ expect(video.style.objectFit).toBe("cover");
27
+ expect(video.style.maxWidth).toBe("none");
28
+ expect(video.style.transform).toBe("translate(-50%, -50%) rotate(90deg)");
29
+ expect(parseFloat(video.style.width)).toBeCloseTo(640 / 480 * 100);
30
+ expect(parseFloat(video.style.height)).toBeCloseTo(480 / 640 * 100);
31
+ });
32
+ test("a stream at another size keeps the recorded aspect ratio", () => {
33
+ act(() => root.render(<BrowserCameraPreview deviceId="A" rotation={0} width={640} height={480} />));
34
+ const video = container.querySelector("video")!;
35
+ Object.defineProperty(video, "videoWidth", { value: 1280 });
36
+ Object.defineProperty(video, "videoHeight", { value: 720 });
37
+ act(() => video.dispatchEvent(new Event("loadedmetadata")));
38
+ expect(video.parentElement!.style.aspectRatio).toBe("640 / 480");
39
+ expect(video.style.width).toBe("100%");
40
+ expect(video.style.objectFit).toBe("cover");
41
+ });
42
+ test("the reference ghost sits over the video with its rotation and opacity", () => {
43
+ act(() => root.render(<BrowserCameraPreview deviceId="wrist" rotation={90} ghostSrc="blob:ref" ghostOpacity={0.3} />));
44
+ const video = container.querySelector("video")!, ghost = container.querySelector("img")!;
45
+ expect(ghost.getAttribute("src")).toBe("blob:ref");
46
+ expect(ghost.style.opacity).toBe("0.3");
47
+ expect(ghost.style.transform).toBe(video.style.transform);
48
+ expect(ghost.style.width).toBe(video.style.width);
49
+ act(() => root.render(<BrowserCameraPreview deviceId="wrist" />));
50
+ expect(container.querySelector("img")).toBeNull();
51
+ });
src/components/control/BrowserCameraPreview.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { VideoOff } from "lucide-react";
3
+ import { useCameraStream } from "@/hooks/useCameraStream";
4
+ import { CameraRotation, cameraPreviewGeometry, cameraRotationError } from "@/lib/cameraConfig";
5
+
6
+ interface Props {
7
+ deviceId: string;
8
+ paused?: boolean;
9
+ rotation?: CameraRotation;
10
+ width?: number;
11
+ height?: number;
12
+ className?: string;
13
+ fallback?: string;
14
+ /** Saved reference frame, drawn over the live video as a translucent ghost. */
15
+ ghostSrc?: string;
16
+ ghostOpacity?: number;
17
+ }
18
+
19
+ /**
20
+ * Browser streams contain raw frames, so only browser previews use CSS rotation.
21
+ * The box keeps the recorded capture size's shape even when the browser stream
22
+ * has another size, so the preview frames the scene the way recording will.
23
+ */
24
+ export default function BrowserCameraPreview({
25
+ deviceId,
26
+ paused = false,
27
+ rotation = 0,
28
+ width = 640,
29
+ height = 480,
30
+ className = "",
31
+ fallback,
32
+ ghostSrc,
33
+ ghostOpacity = 0.5,
34
+ }: Props) {
35
+ const rotationError = cameraRotationError({ rotation });
36
+ const { videoRef, hasError } = useCameraStream(deviceId, paused || !!rotationError, { width, height });
37
+ const geometry = rotationError
38
+ ? { aspectRatio: `${width} / ${height}`, videoStyle: {} }
39
+ : cameraPreviewGeometry(width, height, rotation);
40
+ const showVideo = !rotationError && !paused && deviceId && !hasError;
41
+ return (
42
+ <div
43
+ className={`relative overflow-hidden bg-gray-800 ${className}`}
44
+ style={{ aspectRatio: geometry.aspectRatio }}
45
+ >
46
+ {showVideo ? (
47
+ <>
48
+ <video
49
+ ref={videoRef}
50
+ autoPlay
51
+ muted
52
+ playsInline
53
+ style={geometry.videoStyle}
54
+ />
55
+ {ghostSrc && (
56
+ <img
57
+ src={ghostSrc}
58
+ alt=""
59
+ aria-hidden
60
+ className="pointer-events-none"
61
+ style={{ ...geometry.videoStyle, opacity: ghostOpacity }}
62
+ />
63
+ )}
64
+ </>
65
+ ) : (
66
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
67
+ <VideoOff className="w-5 h-5 text-gray-500 mb-1" />
68
+ <span className="text-xs text-gray-500">
69
+ {rotationError ?? fallback ?? (paused ? "Preview paused" : deviceId ? "Preview failed" : "No camera selected")}
70
+ </span>
71
+ </div>
72
+ )}
73
+ </div>
74
+ );
75
+ }
src/components/control/CameraFeed.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import BrowserCameraPreview from "./BrowserCameraPreview";
3
+ import { CameraRotation } from "@/lib/cameraConfig";
4
+
5
+ interface CameraFeedProps {
6
+ deviceId: string;
7
+ label?: string;
8
+ rotation?: CameraRotation;
9
+ width?: number;
10
+ height?: number;
11
+ }
12
+
13
+ const CameraFeed: React.FC<CameraFeedProps> = ({ deviceId, label, rotation, width, height }) => (
14
+ <div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
15
+ <BrowserCameraPreview deviceId={deviceId} rotation={rotation} width={width} height={height} />
16
+ {label && (
17
+ <div className="p-2 text-sm text-gray-300 truncate border-t border-gray-800">{label}</div>
18
+ )}
19
+ </div>
20
+ );
21
+
22
+ export default CameraFeed;
src/components/control/CameraRotationSelect.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useId } from "react";
2
+ import { Label } from "@/components/ui/label";
3
+ import { CAMERA_ROTATIONS, CameraRotation, cameraRotation } from "@/lib/cameraConfig";
4
+
5
+ interface Props {
6
+ rotation: CameraRotation | null;
7
+ onChange: (rotation: CameraRotation) => void;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export default function CameraRotationSelect({ rotation, onChange, disabled }: Props) {
12
+ const id = useId();
13
+ return (
14
+ <div className="flex items-center gap-2 text-xs">
15
+ <Label htmlFor={id} className="text-gray-400">Rotation</Label>
16
+ <select
17
+ id={id}
18
+ value={rotation ?? ""}
19
+ disabled={disabled}
20
+ onChange={(event) => onChange(cameraRotation(Number(event.target.value)))}
21
+ className="bg-gray-800 border border-gray-700 rounded text-white text-xs h-7 px-2"
22
+ >
23
+ {rotation === null && <option value="" disabled>Choose rotation</option>}
24
+ {CAMERA_ROTATIONS.map((angle) => (
25
+ <option key={angle} value={angle}>{angle}° clockwise</option>
26
+ ))}
27
+ </select>
28
+ </div>
29
+ );
30
+ }
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;