8bitkick commited on
Commit
b9d9e30
·
1 Parent(s): bf33112

URDF renders!

Browse files
.gitignore CHANGED
@@ -1,3 +1,4 @@
1
  __pycache__/
2
  *.egg-info/
3
  build/*
 
 
1
  __pycache__/
2
  *.egg-info/
3
  build/*
4
+ reachy_mini_app_example/assets/*.stl
pyproject.toml CHANGED
@@ -21,4 +21,4 @@ reachy_mini_app_example = "reachy_mini_app_example.main:ExampleApp"
21
  packages = ["reachy_mini_app_example"]
22
 
23
  [tool.setuptools.package-data]
24
- reachy_mini_app_example = ["*.html", "*.css", "*.js"]
 
21
  packages = ["reachy_mini_app_example"]
22
 
23
  [tool.setuptools.package-data]
24
+ reachy_mini_app_example = ["*.html", "*.css", "*.js", "*.py", "assets/*.stl"]
reachy_mini_app_example/main.py CHANGED
@@ -17,11 +17,41 @@ class ExampleApp(ReachyMiniApp):
17
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
18
  def start_static_server(host="127.0.0.1", port=8080, directory=None):
19
  directory = directory or Path(__file__).resolve().parent
20
- handler = partial(SimpleHTTPRequestHandler, directory=str(directory))
21
- server = HTTPServer((host, port), handler)
22
- print(f"🚀 Web server: http://{host}:{port}")
23
- print("🔌 WebSocket: ws://127.0.0.1:8000/api/state/ws/full")
24
- print("📄 Open: http://127.0.0.1:8080/stream.html\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  server.serve_forever()
26
 
27
  threading.Thread(
 
17
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
18
  def start_static_server(host="127.0.0.1", port=8080, directory=None):
19
  directory = directory or Path(__file__).resolve().parent
20
+
21
+ # Use local assets folder
22
+ assets_path = directory / "assets"
23
+
24
+ # Download assets if they don't exist
25
+ if not assets_path.exists() or not list(assets_path.glob("*.stl")):
26
+ print("📥 Assets not found, downloading...")
27
+ from . import setup_assets
28
+ setup_assets.setup_assets()
29
+
30
+ # Create custom handler that serves from both directories
31
+ class CustomHandler(SimpleHTTPRequestHandler):
32
+ def __init__(self, *args, **kwargs):
33
+ super().__init__(*args, directory=str(directory), **kwargs)
34
+
35
+ def translate_path(self, path):
36
+ # Parse the URL to remove query parameters
37
+ from urllib.parse import urlparse, unquote
38
+ path = urlparse(path).path
39
+ path = unquote(path)
40
+
41
+ # If requesting /assets/, serve from local assets folder
42
+ if path.startswith('/assets/'):
43
+ # Remove '/assets/' prefix and any leading slashes
44
+ asset_file = path[8:].lstrip('/')
45
+ asset_full_path = assets_path / asset_file
46
+ return str(asset_full_path)
47
+ # Otherwise serve from app directory
48
+ return super().translate_path(path)
49
+
50
+ server = HTTPServer((host, port), CustomHandler)
51
+
52
+ print("📄 Open: http://127.0.0.1:8080/stream.html")
53
+ print("📄 3D Viz: http://127.0.0.1:8080/robot_viz.html")
54
+ print(f"📦 Serving assets from: {assets_path}\n")
55
  server.serve_forever()
56
 
57
  threading.Thread(
reachy_mini_app_example/robot_viz.html ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Reachy Mini 3D Visualization</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
11
+ overflow: hidden;
12
+ }
13
+ #container {
14
+ width: 100vw;
15
+ height: 100vh;
16
+ }
17
+ #status {
18
+ position: absolute;
19
+ top: 10px;
20
+ left: 10px;
21
+ background: rgba(255, 255, 255, 0.9);
22
+ padding: 10px 15px;
23
+ border-radius: 5px;
24
+ font-size: 14px;
25
+ z-index: 100;
26
+ }
27
+ .ok { color: #0a0; }
28
+ .err { color: #a00; }
29
+ #info {
30
+ position: absolute;
31
+ bottom: 10px;
32
+ left: 10px;
33
+ background: rgba(255, 255, 255, 0.9);
34
+ padding: 10px 15px;
35
+ border-radius: 5px;
36
+ font-size: 12px;
37
+ z-index: 100;
38
+ max-width: 300px;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div id="status">Loading...</div>
44
+ <div id="info">
45
+ <strong>Controls:</strong><br>
46
+ • Left click + drag to rotate<br>
47
+ • Right click + drag to pan<br>
48
+ • Scroll to zoom
49
+ </div>
50
+ <div id="container"></div>
51
+
52
+ <!-- Three.js and URDFLoader -->
53
+ <script type="importmap">
54
+ {
55
+ "imports": {
56
+ "three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",
57
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/"
58
+ }
59
+ }
60
+ </script>
61
+
62
+ <script type="module">
63
+ import * as THREE from 'three';
64
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
65
+ import URDFLoader from 'https://cdn.jsdelivr.net/npm/urdf-loader@0.12.3/+esm';
66
+
67
+ // WebSocket URL - can be changed via URL parameter
68
+ const WS_URL = new URLSearchParams(location.search).get("ws")
69
+ || "ws://127.0.0.1:8000/api/state/ws/full";
70
+
71
+ const URDF_URL = new URLSearchParams(location.search).get("urdf")
72
+ || "http://127.0.0.1:8000/api/kinematics/urdf";
73
+
74
+ const statusEl = document.getElementById("status");
75
+
76
+ // Scene setup
77
+ const scene = new THREE.Scene();
78
+ scene.background = new THREE.Color(0xf0f0f0);
79
+
80
+ // Camera setup
81
+ const camera = new THREE.PerspectiveCamera(
82
+ 75,
83
+ window.innerWidth / window.innerHeight,
84
+ 0.01,
85
+ 100
86
+ );
87
+ camera.position.set(0.5, 0.5, 0.5);
88
+
89
+ // Renderer setup
90
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
91
+ renderer.setSize(window.innerWidth, window.innerHeight);
92
+ renderer.shadowMap.enabled = true;
93
+ document.getElementById('container').appendChild(renderer.domElement);
94
+
95
+ // Controls
96
+ const controls = new OrbitControls(camera, renderer.domElement);
97
+ controls.enableDamping = true;
98
+ controls.dampingFactor = 0.05;
99
+
100
+ // Lighting
101
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
102
+ scene.add(ambientLight);
103
+
104
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
105
+ directionalLight.position.set(1, 2, 1);
106
+ directionalLight.castShadow = true;
107
+ scene.add(directionalLight);
108
+
109
+ // Grid helper
110
+ const gridHelper = new THREE.GridHelper(1, 10);
111
+ scene.add(gridHelper);
112
+
113
+ // Robot model
114
+ let robot = null;
115
+ let jointMap = {};
116
+
117
+ // Load URDF
118
+ statusEl.innerHTML = '📥 Loading URDF...';
119
+
120
+ // Fetch the URDF JSON from the daemon
121
+ fetch(URDF_URL)
122
+ .then(response => response.json())
123
+ .then(data => {
124
+ if (!data.urdf) {
125
+ throw new Error('No URDF found in response');
126
+ }
127
+
128
+ let urdfXml = data.urdf;
129
+
130
+ // DON'T modify the URDF - keep package:// URIs as-is
131
+ // The loader will use the packages map to resolve them
132
+ console.log('URDF snippet:', urdfXml.substring(0, 1000));
133
+
134
+ // Create a blob URL from the URDF XML string
135
+ const blob = new Blob([urdfXml], { type: 'application/xml' });
136
+ const blobUrl = URL.createObjectURL(blob);
137
+
138
+ // Create loader with custom package paths that resolve to OUR local server
139
+ // serving the assets from the reachy_mini package
140
+ const loader = new URDFLoader();
141
+ loader.packages = {
142
+ 'assets': `${window.location.origin}/assets/`,
143
+ 'reachy_mini_description': `${window.location.origin}/assets/`
144
+ };
145
+
146
+ console.log('Package resolution:', loader.packages);
147
+
148
+ // Add a custom fetch function to handle mesh loading with better error handling
149
+ const originalFetch = loader.fetchOptions?.fetch || fetch;
150
+ loader.fetchOptions = {
151
+ ...loader.fetchOptions,
152
+ fetch: (url, ...args) => {
153
+ console.log('Fetching mesh:', url);
154
+ return originalFetch(url, ...args).then(response => {
155
+ if (!response.ok) {
156
+ console.error(`Failed to fetch ${url}: ${response.status}`);
157
+ }
158
+ return response;
159
+ });
160
+ }
161
+ };
162
+
163
+ loader.load(
164
+ blobUrl,
165
+ (urdfRobot) => {
166
+ robot = urdfRobot;
167
+ scene.add(robot);
168
+
169
+ // Build joint map for easy access
170
+ robot.traverse((child) => {
171
+ if (child.isURDFJoint) {
172
+ jointMap[child.name] = child;
173
+ }
174
+ });
175
+
176
+ // Clean up the blob URL
177
+ URL.revokeObjectURL(blobUrl);
178
+
179
+ statusEl.innerHTML = '✅ URDF loaded (some meshes may be missing). Connecting to WebSocket...';
180
+ connectWebSocket();
181
+ },
182
+ undefined,
183
+ (error) => {
184
+ statusEl.innerHTML = `<span class="err">❌ Error loading URDF: ${error.message}</span>`;
185
+ console.error('URDF load error:', error);
186
+ URL.revokeObjectURL(blobUrl);
187
+ }
188
+ );
189
+ })
190
+ .catch(error => {
191
+ statusEl.innerHTML = `<span class="err">❌ Error fetching URDF: ${error.message}</span>`;
192
+ console.error('URDF fetch error:', error);
193
+ });
194
+
195
+ // WebSocket connection
196
+ let ws = null;
197
+
198
+ function connectWebSocket() {
199
+ ws = new WebSocket(WS_URL);
200
+
201
+ ws.onopen = () => {
202
+ statusEl.innerHTML = '<span class="ok">🟢 Connected</span>';
203
+ };
204
+
205
+ ws.onmessage = (event) => {
206
+ try {
207
+ const data = JSON.parse(event.data);
208
+ updateRobotJoints(data);
209
+ } catch (e) {
210
+ console.error('Error parsing WebSocket message:', e);
211
+ }
212
+ };
213
+
214
+ ws.onerror = (error) => {
215
+ statusEl.innerHTML = '<span class="err">❌ WebSocket error</span>';
216
+ console.error('WebSocket error:', error);
217
+ };
218
+
219
+ ws.onclose = () => {
220
+ statusEl.innerHTML = '<span class="err">🔴 Disconnected. Reconnecting...</span>';
221
+ setTimeout(connectWebSocket, 3000);
222
+ };
223
+ }
224
+
225
+ // Update robot joints from WebSocket data
226
+ function updateRobotJoints(data) {
227
+ if (!robot || !data.joints) return;
228
+
229
+ // Update each joint position
230
+ for (const [jointName, jointData] of Object.entries(data.joints)) {
231
+ const joint = jointMap[jointName];
232
+ if (joint && jointData.present_position !== undefined) {
233
+ // Convert from degrees to radians if needed
234
+ const angle = jointData.present_position * (Math.PI / 180);
235
+ joint.setJointValue(angle);
236
+ }
237
+ }
238
+ }
239
+
240
+ // Animation loop
241
+ function animate() {
242
+ requestAnimationFrame(animate);
243
+ controls.update();
244
+ renderer.render(scene, camera);
245
+ }
246
+
247
+ // Handle window resize
248
+ window.addEventListener('resize', () => {
249
+ camera.aspect = window.innerWidth / window.innerHeight;
250
+ camera.updateProjectionMatrix();
251
+ renderer.setSize(window.innerWidth, window.innerHeight);
252
+ });
253
+
254
+ // Start animation
255
+ animate();
256
+ </script>
257
+ </body>
258
+ </html>
reachy_mini_app_example/setup_assets.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Download URDF mesh assets from the reachy_mini repository using Git LFS.
4
+
5
+ There is definitely a more elegant way to do this...
6
+ """
7
+ import subprocess
8
+ import shutil
9
+ from pathlib import Path
10
+
11
+
12
+ def setup_assets():
13
+ """Download STL assets from the reachy_mini repository."""
14
+ assets_dir = Path(__file__).parent / "assets"
15
+
16
+ # If assets already exist and have .stl files, skip
17
+ if assets_dir.exists() and list(assets_dir.glob("*.stl")):
18
+ print(f"✅ Assets already exist in {assets_dir}")
19
+ return
20
+
21
+ print("📥 Downloading mesh assets from reachy_mini repository...")
22
+
23
+ # Create assets directory
24
+ assets_dir.mkdir(exist_ok=True)
25
+
26
+ # Clone just the assets folder using sparse checkout
27
+ temp_dir = Path(__file__).parent / ".temp_repo"
28
+ temp_dir.mkdir(exist_ok=True)
29
+
30
+ try:
31
+ # Initialize git repo
32
+ subprocess.run(
33
+ ["git", "init"],
34
+ cwd=temp_dir,
35
+ check=False,
36
+ capture_output=True
37
+ )
38
+
39
+ # Add remote
40
+ subprocess.run(
41
+ ["git", "remote", "add", "origin",
42
+ "https://github.com/pollen-robotics/reachy_mini.git"],
43
+ cwd=temp_dir,
44
+ check=False,
45
+ capture_output=True
46
+ )
47
+
48
+ # Configure sparse checkout
49
+ subprocess.run(
50
+ ["git", "config", "core.sparseCheckout", "true"],
51
+ cwd=temp_dir,
52
+ check=False,
53
+ capture_output=True
54
+ )
55
+
56
+ # Specify the path to checkout
57
+ sparse_file = temp_dir / ".git" / "info" / "sparse-checkout"
58
+ sparse_file.parent.mkdir(parents=True, exist_ok=True)
59
+ sparse_file.write_text("src/reachy_mini/descriptions/reachy_mini/urdf/assets\n")
60
+
61
+ # Pull the files
62
+ result = subprocess.run(
63
+ ["git", "pull", "--depth=1", "origin", "develop"],
64
+ cwd=temp_dir,
65
+ capture_output=True,
66
+ text=True
67
+ )
68
+
69
+ if result.returncode != 0:
70
+ print(f"⚠️ Git pull failed: {result.stderr}")
71
+ print("Trying direct download instead...")
72
+ download_directly(assets_dir)
73
+ else:
74
+ # Copy assets
75
+ source_assets = temp_dir / "src" / "reachy_mini" / "descriptions" / "reachy_mini" / "urdf" / "assets"
76
+ if source_assets.exists():
77
+ for file in source_assets.glob("*.stl"):
78
+ shutil.copy(file, assets_dir / file.name)
79
+ print(f"✅ Downloaded {len(list(assets_dir.glob('*.stl')))} STL files")
80
+ else:
81
+ print("⚠️ Assets folder not found in cloned repo")
82
+ download_directly(assets_dir)
83
+
84
+ finally:
85
+ # Cleanup
86
+ if temp_dir.exists():
87
+ shutil.rmtree(temp_dir, ignore_errors=True)
88
+
89
+
90
+ def download_directly(assets_dir):
91
+ """Fallback: download files directly from GitHub raw URL."""
92
+ import urllib.request
93
+
94
+ # List of STL files (from the GitHub repo structure you showed)
95
+ stl_files = [
96
+ "5w_speaker.stl", "antenna.stl", "antenna_body_3dprint.stl",
97
+ "antenna_holder_l_3dprint.stl", "antenna_holder_r_3dprint.stl",
98
+ "antenna_interface_3dprint.stl", "arducam.stl", "arm.stl",
99
+ "b3b_eh.stl", "b3b_eh_1.stl", "ball.stl", "bearing_85x110x13.stl",
100
+ "big_lens.stl", "big_lens_d40.stl", "body_down_3dprint.stl",
101
+ "body_foot_3dprint.stl", "body_turning_3dprint.stl",
102
+ "dc15_a01_horn_dummy.stl", "dc15_a01_led_cap2_dummy.stl",
103
+ "glasses_dolder_3dprint.stl", "head_back_3dprint.stl",
104
+ "head_front_3dprint.stl", "head_mic_3dprint.stl",
105
+ "lens_cap_d40_3dprint.stl", "m12_fisheye_lens_1_8mm.stl",
106
+ "mp01062_stewart_arm_3.stl", "neck_reference_3dprint.stl",
107
+ "phs_1_7x20_5_dc10.stl", "phs_1_7x20_5_dc10_1.stl",
108
+ "phs_1_7x20_5_dc10_3.stl", "pp01102_arducam_carter.stl",
109
+ "small_lens_d30.stl", "stewart_link_ball.stl",
110
+ "stewart_link_ball_2.stl", "stewart_link_rod.stl",
111
+ "stewart_tricap_3dprint.stl"
112
+ ]
113
+
114
+ base_url = "https://github.com/pollen-robotics/reachy_mini/raw/develop/src/reachy_mini/descriptions/reachy_mini/urdf/assets"
115
+
116
+ print(f"📥 Downloading {len(stl_files)} STL files directly...")
117
+ success_count = 0
118
+
119
+ for filename in stl_files:
120
+ url = f"{base_url}/{filename}"
121
+ dest = assets_dir / filename
122
+ try:
123
+ urllib.request.urlretrieve(url, dest)
124
+ success_count += 1
125
+ print(f" ✓ {filename}")
126
+ except Exception as e:
127
+ print(f" ✗ {filename}: {e}")
128
+
129
+ print(f"✅ Downloaded {success_count}/{len(stl_files)} files")
130
+
131
+
132
+ if __name__ == "__main__":
133
+ setup_assets()