8bitkick commited on
Commit
d82254a
·
1 Parent(s): e6b41f4
reachy_mini_app_example/robot_viz.html CHANGED
@@ -131,421 +131,10 @@
131
  </script>
132
 
133
  <script type="module">
134
- import * as THREE from 'three';
135
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
136
- import URDFLoader from 'https://cdn.jsdelivr.net/npm/urdf-loader@0.12.3/+esm';
137
- import { RobotStateClient } from './robot_state.js';
138
-
139
- // WebSocket URL - can be changed via URL parameter
140
- const WS_URL = new URLSearchParams(location.search).get("ws")
141
- || "ws://127.0.0.1:8000/api/state/ws/full";
142
-
143
- const URDF_URL = new URLSearchParams(location.search).get("urdf")
144
- || "http://127.0.0.1:8000/api/kinematics/urdf";
145
-
146
- const STL_BASE_URL = new URLSearchParams(location.search).get("stl_base")
147
- || "http://127.0.0.1:8000/api/kinematics/stl";
148
-
149
- const statusEl = document.getElementById("status");
150
- const jointsListEl = document.getElementById("joints-list");
151
-
152
- // Scene setup
153
- const scene = new THREE.Scene();
154
-
155
- // Retro-futuristic Arizona sunset sky (deep purple → orange)
156
- const bgCanvas = document.createElement('canvas');
157
- bgCanvas.width = 1024;
158
- bgCanvas.height = 1024;
159
- const bgCtx = bgCanvas.getContext('2d');
160
- const bgGradient = bgCtx.createLinearGradient(0, 0, 0, bgCanvas.height);
161
- bgGradient.addColorStop(0.0, '#f7a072'); // warm orange
162
- bgGradient.addColorStop(0.08, '#e97a5b'); // orange-red
163
- bgGradient.addColorStop(0.18, '#cf4958'); // rose-orange
164
- bgGradient.addColorStop(0.32, '#8e2a6a'); // purple-rose
165
- bgGradient.addColorStop(0.55, '#4b136f'); // rich purple-magenta
166
- bgGradient.addColorStop(0.78, '#1a0b4b'); // deep purple
167
- bgGradient.addColorStop(1.0, '#0b032d'); // very deep purple
168
- bgCtx.fillStyle = bgGradient;
169
- bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
170
- const bgTexture = new THREE.CanvasTexture(bgCanvas);
171
- if (THREE?.SRGBColorSpace) {
172
- bgTexture.colorSpace = THREE.SRGBColorSpace;
173
- }
174
- scene.background = bgTexture;
175
-
176
- // Camera setup
177
- const camera = new THREE.PerspectiveCamera(
178
- 75,
179
- window.innerWidth / window.innerHeight,
180
- 0.01,
181
- 100
182
- );
183
- camera.position.set(0.3, 0.3, 0.3);
184
- camera.lookAt(0, 0.1, 0);
185
-
186
- // Renderer setup
187
- const renderer = new THREE.WebGLRenderer({ antialias: true });
188
- renderer.setSize(window.innerWidth, window.innerHeight);
189
- renderer.shadowMap.enabled = true;
190
- document.getElementById('container').appendChild(renderer.domElement);
191
-
192
- // Controls
193
- const controls = new OrbitControls(camera, renderer.domElement);
194
- controls.target.set(0, 0.1, 0);
195
- controls.enableDamping = true;
196
- controls.dampingFactor = 0.05;
197
-
198
- // Lighting
199
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
200
- scene.add(ambientLight);
201
-
202
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
203
- directionalLight.position.set(1, 2, 1);
204
- directionalLight.castShadow = true;
205
- scene.add(directionalLight);
206
-
207
- // Spotlight to illuminate the robot nicely
208
- const spotLight = new THREE.SpotLight(0xffffff, 1.5);
209
- spotLight.position.set(0.5, 0.5, 0.5);
210
- spotLight.angle = Math.PI / 4;
211
- spotLight.penumbra = 0.3;
212
- spotLight.decay = 2;
213
- spotLight.distance = 3;
214
- spotLight.castShadow = true;
215
- spotLight.target.position.set(0, 0.1, 0);
216
- scene.add(spotLight);
217
- scene.add(spotLight.target);
218
-
219
- // Grid helper
220
- const gridHelper = new THREE.GridHelper(1, 10);
221
- scene.add(gridHelper);
222
-
223
- // Robot model
224
- let robot = null;
225
- let jointMap = {};
226
-
227
- // Chart setup
228
- const MAX_SAMPLES = 40;
229
- const chartData = {
230
- labels: Array(MAX_SAMPLES).fill(''),
231
- datasets: []
232
- };
233
-
234
- const jointColors = {
235
- 'head yaw': '#ff6384',
236
- 'head pitch': '#36a2eb',
237
- 'head roll': '#ffce56',
238
- 'left antenna': '#4bc0c0',
239
- 'right antenna': '#9966ff',
240
- 'body yaw': '#ff9f40'
241
- };
242
 
243
- const ctx = document.getElementById('chart').getContext('2d');
244
- const chart = new Chart(ctx, {
245
- type: 'line',
246
- data: chartData,
247
- options: {
248
- responsive: true,
249
- maintainAspectRatio: false,
250
- animation: false,
251
- plugins: {
252
- legend: {
253
- display: false
254
- }
255
- },
256
- scales: {
257
- x: {
258
- display: false
259
- },
260
- y: {
261
- ticks: { color: '#ffffff' },
262
- grid: { color: 'rgba(255, 255, 255, 0.1)' }
263
- }
264
- },
265
- elements: {
266
- point: { radius: 0 },
267
- line: { borderWidth: 2 }
268
- }
269
- }
270
- });
271
-
272
- // Load URDF
273
- statusEl.innerHTML = '📥 Loading URDF...';
274
-
275
- // Fetch the URDF JSON from the daemon
276
- fetch(URDF_URL)
277
- .then(response => response.json())
278
- .then(data => {
279
- if (!data.urdf) {
280
- throw new Error('No URDF found in response');
281
- }
282
-
283
- let urdfXml = data.urdf;
284
-
285
- // DON'T modify the URDF - keep package:// URIs as-is
286
- // The loader will use the packages map to resolve them
287
- console.log('URDF snippet:', urdfXml.substring(0, 1000));
288
-
289
- // Create a blob URL from the URDF XML string
290
- const blob = new Blob([urdfXml], { type: 'application/xml' });
291
- const blobUrl = URL.createObjectURL(blob);
292
-
293
- // Create loader with custom package paths that resolve to the REST API
294
- // Note: Don't include trailing slash - the URDF loader will add it
295
- const loader = new URDFLoader();
296
- loader.packages = {
297
- 'assets': STL_BASE_URL,
298
- 'reachy_mini_description': STL_BASE_URL
299
- };
300
-
301
- console.log('Package resolution:', loader.packages);
302
- console.log('STL files will be fetched from REST API:', STL_BASE_URL);
303
-
304
- // Add a custom fetch function to handle mesh loading with better error handling
305
- const originalFetch = loader.fetchOptions?.fetch || fetch;
306
- loader.fetchOptions = {
307
- ...loader.fetchOptions,
308
- fetch: (url, ...args) => {
309
- console.log('Fetching mesh via API:', url);
310
- return originalFetch(url, ...args).then(response => {
311
- if (!response.ok) {
312
- console.error(`Failed to fetch ${url}: ${response.status}`);
313
- }
314
- return response;
315
- });
316
- }
317
- };
318
-
319
- loader.load(
320
- blobUrl,
321
- (urdfRobot) => {
322
- robot = urdfRobot;
323
-
324
- robot.rotation.x = -Math.PI / 2; // Rotate 90 degrees around X axis
325
-
326
- scene.add(robot);
327
-
328
- // Build joint map
329
- robot.traverse((child) => {
330
- if (child.isURDFJoint) {
331
- jointMap[child.name] = child;
332
- }
333
- });
334
-
335
- // Convert robot to blue wireframe - delay to allow meshes to load
336
- setTimeout(() => {
337
- let meshCount = 0;
338
- scene.traverse((child) => {
339
- if (child.type === 'Mesh' || child.isMesh) {
340
- meshCount++;
341
- // Apply new wireframe material
342
- child.material = new THREE.MeshBasicMaterial({
343
- color: 0x0044ff,
344
- wireframe: true
345
- });
346
- child.material.needsUpdate = true;
347
- }
348
- });
349
- console.log(`✨ Applied wireframe to ${meshCount} meshes`);
350
- }, 2500);
351
-
352
- // Clean up the blob URL
353
- URL.revokeObjectURL(blobUrl);
354
-
355
- statusEl.innerHTML = '✅ URDF loaded (some meshes may be missing). Connecting to WebSocket...';
356
- connectWebSocket();
357
- },
358
- undefined,
359
- (error) => {
360
- statusEl.innerHTML = `<span class="err">❌ Error loading URDF: ${error.message}</span>`;
361
- console.error('URDF load error:', error);
362
- URL.revokeObjectURL(blobUrl);
363
- }
364
- );
365
- })
366
- .catch(error => {
367
- statusEl.innerHTML = `<span class="err">❌ Error fetching URDF: ${error.message}</span>`;
368
- console.error('URDF fetch error:', error);
369
- });
370
-
371
- // WebSocket connection using shared client
372
- const robotClient = new RobotStateClient(WS_URL);
373
-
374
- function connectWebSocket() {
375
- robotClient.onStateChange((event, data) => {
376
- if (event === 'connected') {
377
- statusEl.innerHTML = '<span class="ok"><span class="connection-led"></span> Reachy Mini</span>';
378
- } else if (event === 'disconnected') {
379
- statusEl.innerHTML = '<span class="err"><span class="connection-led"></span>Disconnected. Reconnecting...</span>';
380
- } else if (event === 'error') {
381
- statusEl.innerHTML = '<span class="err"><span class="connection-led"></span>WebSocket error</span>';
382
- } else if (event === 'data') {
383
- updateRobotJoints(data);
384
- updateJointsDisplay(data);
385
- }
386
- });
387
-
388
- robotClient.connect();
389
- }
390
-
391
- // Update robot joints from WebSocket data
392
- function updateRobotJoints(data) {
393
- if (!robot) return;
394
-
395
- // Log the data structure once to debug
396
- if (!updateRobotJoints.logged) {
397
- console.log('WebSocket data structure:', data);
398
- console.log('Available joints in URDF:', Object.keys(jointMap));
399
- updateRobotJoints.logged = true;
400
- }
401
-
402
- // Update head pose if available
403
- if (data.head_pose && jointMap['yaw_body']) {
404
- // head_pose contains x, y, z, roll, pitch, yaw
405
- // Map yaw to the yaw_body joint
406
- if (data.head_pose.yaw !== undefined) {
407
- jointMap['yaw_body'].setJointValue(data.head_pose.yaw);
408
- }
409
- }
410
-
411
- // Update antennas if available
412
- if (data.antennas_position && Array.isArray(data.antennas_position)) {
413
- if (jointMap['left_antenna'] && data.antennas_position[0] !== undefined) {
414
- jointMap['left_antenna'].setJointValue(data.antennas_position[0]);
415
- }
416
- if (jointMap['right_antenna'] && data.antennas_position[1] !== undefined) {
417
- jointMap['right_antenna'].setJointValue(data.antennas_position[1]);
418
- }
419
- }
420
-
421
- // Update body yaw if available
422
- if (data.body_yaw !== undefined && jointMap['yaw_body']) {
423
- jointMap['yaw_body'].setJointValue(data.body_yaw);
424
- }
425
- }
426
-
427
- // Update the joints display overlay
428
- function updateJointsDisplay(data) {
429
- let html = '';
430
- const jointValues = {};
431
-
432
- // Display head pose
433
- if (data.head_pose) {
434
- const yaw = data.head_pose.yaw * 180 / Math.PI;
435
- const pitch = data.head_pose.pitch * 180 / Math.PI;
436
- const roll = data.head_pose.roll * 180 / Math.PI;
437
-
438
- html += `<div class="joint-item">
439
- <span class="joint-name">
440
- <span class="joint-color-dot" style="background-color: ${jointColors['head yaw'] || '#ffffff'}"></span>
441
- head yaw
442
- </span>
443
- <span class="joint-value">${yaw.toFixed(2)}°</span>
444
- </div>`;
445
- html += `<div class="joint-item">
446
- <span class="joint-name">
447
- <span class="joint-color-dot" style="background-color: ${jointColors['head pitch'] || '#ffffff'}"></span>
448
- head pitch
449
- </span>
450
- <span class="joint-value">${pitch.toFixed(2)}°</span>
451
- </div>`;
452
- html += `<div class="joint-item">
453
- <span class="joint-name">
454
- <span class="joint-color-dot" style="background-color: ${jointColors['head roll'] || '#ffffff'}"></span>
455
- head roll
456
- </span>
457
- <span class="joint-value">${roll.toFixed(2)}°</span>
458
- </div>`;
459
-
460
- jointValues['head yaw'] = yaw;
461
- jointValues['head pitch'] = pitch;
462
- jointValues['head roll'] = roll;
463
- }
464
-
465
- // Display antennas
466
- if (data.antennas_position && Array.isArray(data.antennas_position)) {
467
- const leftAntenna = data.antennas_position[0] * 180 / Math.PI;
468
- const rightAntenna = data.antennas_position[1] * 180 / Math.PI;
469
-
470
- html += `<div class="joint-item">
471
- <span class="joint-name">
472
- <span class="joint-color-dot" style="background-color: ${jointColors['left antenna'] || '#ffffff'}"></span>
473
- left antenna
474
- </span>
475
- <span class="joint-value">${leftAntenna.toFixed(2)}°</span>
476
- </div>`;
477
- html += `<div class="joint-item">
478
- <span class="joint-name">
479
- <span class="joint-color-dot" style="background-color: ${jointColors['right antenna'] || '#ffffff'}"></span>
480
- right antenna
481
- </span>
482
- <span class="joint-value">${rightAntenna.toFixed(2)}°</span>
483
- </div>`;
484
-
485
- jointValues['left antenna'] = leftAntenna;
486
- jointValues['right antenna'] = rightAntenna;
487
- }
488
-
489
- // Display body yaw
490
- if (data.body_yaw !== undefined) {
491
- const bodyYaw = data.body_yaw * 180 / Math.PI;
492
- html += `<div class="joint-item">
493
- <span class="joint-name">
494
- <span class="joint-color-dot" style="background-color: ${jointColors['body yaw'] || '#ffffff'}"></span>
495
- body yaw
496
- </span>
497
- <span class="joint-value">${bodyYaw.toFixed(2)}°</span>
498
- </div>`;
499
-
500
- jointValues['body yaw'] = bodyYaw;
501
- }
502
-
503
- jointsListEl.innerHTML = html || 'No joint data';
504
-
505
- // Update chart
506
- updateChart(jointValues);
507
- }
508
-
509
- // Update chart with new joint values
510
- function updateChart(jointValues) {
511
- // Ensure all datasets exist
512
- for (const [name, value] of Object.entries(jointValues)) {
513
- let dataset = chart.data.datasets.find(ds => ds.label === name);
514
- if (!dataset) {
515
- dataset = {
516
- label: name,
517
- data: Array(MAX_SAMPLES).fill(null),
518
- borderColor: jointColors[name] || '#ffffff',
519
- backgroundColor: 'transparent',
520
- tension: 0.4
521
- };
522
- chart.data.datasets.push(dataset);
523
- }
524
-
525
- // Shift and add new value
526
- dataset.data.shift();
527
- dataset.data.push(value);
528
- }
529
-
530
- chart.update('none'); // Update without animation
531
- }
532
-
533
- // Animation loop
534
- function animate() {
535
- requestAnimationFrame(animate);
536
- controls.update();
537
- renderer.render(scene, camera);
538
- }
539
-
540
- // Handle window resize
541
- window.addEventListener('resize', () => {
542
- camera.aspect = window.innerWidth / window.innerHeight;
543
- camera.updateProjectionMatrix();
544
- renderer.setSize(window.innerWidth, window.innerHeight);
545
- });
546
-
547
- // Start animation
548
- animate();
549
  </script>
550
  </body>
551
  </html>
 
131
  </script>
132
 
133
  <script type="module">
134
+ import { App } from './src/App.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ // Initialize the application
137
+ new App();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  </script>
139
  </body>
140
  </html>
reachy_mini_app_example/src/App.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SceneManager } from './SceneManager.js';
2
+ import { RobotManager } from './RobotManager.js';
3
+ import { ChartManager } from './ChartManager.js';
4
+ import { UIManager } from './UIManager.js';
5
+ import { WebSocketManager } from './WebSocketManager.js';
6
+
7
+ export class App {
8
+ constructor() {
9
+ this.sceneManager = null;
10
+ this.robotManager = null;
11
+ this.chartManager = null;
12
+ this.uiManager = null;
13
+ this.webSocketManager = null;
14
+
15
+ this.init();
16
+ }
17
+
18
+ async init() {
19
+ // Initialize DOM elements
20
+ const container = document.getElementById('container');
21
+ const statusEl = document.getElementById('status');
22
+ const jointsListEl = document.getElementById('joints-list');
23
+ const chartCanvas = document.getElementById('chart');
24
+
25
+ // Initialize managers
26
+ this.sceneManager = new SceneManager(container);
27
+ this.uiManager = new UIManager(statusEl, jointsListEl);
28
+ this.chartManager = new ChartManager(chartCanvas);
29
+
30
+ // Initialize robot manager with status callback
31
+ this.robotManager = new RobotManager((message) => {
32
+ this.uiManager.updateStatus(message);
33
+ });
34
+
35
+ // Initialize WebSocket manager
36
+ this.webSocketManager = new WebSocketManager(
37
+ this.uiManager,
38
+ this.robotManager,
39
+ this.chartManager
40
+ );
41
+
42
+ try {
43
+ // Load robot
44
+ const robot = await this.robotManager.loadRobot();
45
+ this.sceneManager.add(robot);
46
+
47
+ // Connect WebSocket
48
+ this.webSocketManager.connect();
49
+
50
+ } catch (error) {
51
+ console.error('Failed to initialize app:', error);
52
+ }
53
+
54
+ // Start animation loop
55
+ this.sceneManager.animate();
56
+ }
57
+ }
reachy_mini_app_example/src/ChartManager.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class ChartManager {
2
+ constructor(canvasElement) {
3
+ this.canvas = canvasElement;
4
+ this.ctx = canvasElement.getContext('2d');
5
+ this.chart = null;
6
+ this.MAX_SAMPLES = 40;
7
+
8
+ this.jointColors = {
9
+ 'head yaw': '#ff6384',
10
+ 'head pitch': '#36a2eb',
11
+ 'head roll': '#ffce56',
12
+ 'left antenna': '#4bc0c0',
13
+ 'right antenna': '#9966ff',
14
+ 'body yaw': '#ff9f40'
15
+ };
16
+
17
+ this.init();
18
+ }
19
+
20
+ init() {
21
+ const chartData = {
22
+ labels: Array(this.MAX_SAMPLES).fill(''),
23
+ datasets: []
24
+ };
25
+
26
+ this.chart = new Chart(this.ctx, {
27
+ type: 'line',
28
+ data: chartData,
29
+ options: {
30
+ responsive: true,
31
+ maintainAspectRatio: false,
32
+ animation: false,
33
+ plugins: {
34
+ legend: {
35
+ display: false
36
+ }
37
+ },
38
+ scales: {
39
+ x: {
40
+ display: false
41
+ },
42
+ y: {
43
+ ticks: { color: '#ffffff' },
44
+ grid: { color: 'rgba(255, 255, 255, 0.1)' }
45
+ }
46
+ },
47
+ elements: {
48
+ point: { radius: 0 },
49
+ line: { borderWidth: 2 }
50
+ }
51
+ }
52
+ });
53
+ }
54
+
55
+ updateChart(jointValues) {
56
+ // Ensure all datasets exist
57
+ for (const [name, value] of Object.entries(jointValues)) {
58
+ let dataset = this.chart.data.datasets.find(ds => ds.label === name);
59
+ if (!dataset) {
60
+ dataset = {
61
+ label: name,
62
+ data: Array(this.MAX_SAMPLES).fill(null),
63
+ borderColor: this.jointColors[name] || '#ffffff',
64
+ backgroundColor: 'transparent',
65
+ tension: 0.4
66
+ };
67
+ this.chart.data.datasets.push(dataset);
68
+ }
69
+
70
+ // Shift and add new value
71
+ dataset.data.shift();
72
+ dataset.data.push(value);
73
+ }
74
+
75
+ this.chart.update('none'); // Update without animation
76
+ }
77
+ }
reachy_mini_app_example/src/RobotManager.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ import URDFLoader from 'https://cdn.jsdelivr.net/npm/urdf-loader@0.12.3/+esm';
3
+
4
+ export class RobotManager {
5
+ constructor(statusCallback) {
6
+ this.robot = null;
7
+ this.jointMap = {};
8
+ this.statusCallback = statusCallback;
9
+ this.logged = false;
10
+
11
+ // URL configuration
12
+ this.URDF_URL = new URLSearchParams(location.search).get("urdf")
13
+ || "http://127.0.0.1:8000/api/kinematics/urdf";
14
+ this.STL_BASE_URL = new URLSearchParams(location.search).get("stl_base")
15
+ || "http://127.0.0.1:8000/api/kinematics/stl";
16
+ }
17
+
18
+ async loadRobot() {
19
+ this.statusCallback('📥 Loading URDF...');
20
+
21
+ try {
22
+ const response = await fetch(this.URDF_URL);
23
+ const data = await response.json();
24
+
25
+ if (!data.urdf) {
26
+ throw new Error('No URDF found in response');
27
+ }
28
+
29
+ const urdfXml = data.urdf;
30
+ console.log('URDF snippet:', urdfXml.substring(0, 1000));
31
+
32
+ // Create a blob URL from the URDF XML string
33
+ const blob = new Blob([urdfXml], { type: 'application/xml' });
34
+ const blobUrl = URL.createObjectURL(blob);
35
+
36
+ // Create loader with custom package paths
37
+ const loader = new URDFLoader();
38
+ loader.packages = {
39
+ 'assets': this.STL_BASE_URL,
40
+ 'reachy_mini_description': this.STL_BASE_URL
41
+ };
42
+
43
+ console.log('Package resolution:', loader.packages);
44
+ console.log('STL files will be fetched from REST API:', this.STL_BASE_URL);
45
+
46
+ // Add custom fetch function for better error handling
47
+ const originalFetch = loader.fetchOptions?.fetch || fetch;
48
+ loader.fetchOptions = {
49
+ ...loader.fetchOptions,
50
+ fetch: (url, ...args) => {
51
+ console.log('Fetching mesh via API:', url);
52
+ return originalFetch(url, ...args).then(response => {
53
+ if (!response.ok) {
54
+ console.error(`Failed to fetch ${url}: ${response.status}`);
55
+ }
56
+ return response;
57
+ });
58
+ }
59
+ };
60
+
61
+ return new Promise((resolve, reject) => {
62
+ loader.load(
63
+ blobUrl,
64
+ (urdfRobot) => {
65
+ this.robot = urdfRobot;
66
+ this.robot.rotation.x = -Math.PI / 2; // Rotate 90 degrees around X axis
67
+
68
+ // Build joint map
69
+ this.robot.traverse((child) => {
70
+ if (child.isURDFJoint) {
71
+ this.jointMap[child.name] = child;
72
+ }
73
+ });
74
+
75
+ // Convert robot to blue wireframe after delay
76
+ this.applyWireframeMaterial();
77
+
78
+ // Clean up the blob URL
79
+ URL.revokeObjectURL(blobUrl);
80
+
81
+ this.statusCallback('✅ URDF loaded (some meshes may be missing). Connecting to WebSocket...');
82
+ resolve(this.robot);
83
+ },
84
+ undefined,
85
+ (error) => {
86
+ this.statusCallback(`❌ Error loading URDF: ${error.message}`);
87
+ console.error('URDF load error:', error);
88
+ URL.revokeObjectURL(blobUrl);
89
+ reject(error);
90
+ }
91
+ );
92
+ });
93
+ } catch (error) {
94
+ this.statusCallback(`❌ Error fetching URDF: ${error.message}`);
95
+ console.error('URDF fetch error:', error);
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ applyWireframeMaterial() {
101
+ setTimeout(() => {
102
+ let meshCount = 0;
103
+ this.robot.traverse((child) => {
104
+ if (child.type === 'Mesh' || child.isMesh) {
105
+ meshCount++;
106
+ child.material = new THREE.MeshBasicMaterial({
107
+ color: 0x0044ff,
108
+ wireframe: true
109
+ });
110
+ child.material.needsUpdate = true;
111
+ }
112
+ });
113
+ console.log(`✨ Applied wireframe to ${meshCount} meshes`);
114
+ }, 2500);
115
+ }
116
+
117
+ updateJoints(data) {
118
+ if (!this.robot) return;
119
+
120
+ // Log the data structure once for debugging
121
+ if (!this.logged) {
122
+ console.log('WebSocket data structure:', data);
123
+ console.log('Available joints in URDF:', Object.keys(this.jointMap));
124
+ this.logged = true;
125
+ }
126
+
127
+ // Update head pose if available
128
+ if (data.head_pose && this.jointMap['yaw_body']) {
129
+ if (data.head_pose.yaw !== undefined) {
130
+ this.jointMap['yaw_body'].setJointValue(data.head_pose.yaw);
131
+ }
132
+ }
133
+
134
+ // Update antennas if available
135
+ if (data.antennas_position && Array.isArray(data.antennas_position)) {
136
+ if (this.jointMap['left_antenna'] && data.antennas_position[0] !== undefined) {
137
+ this.jointMap['left_antenna'].setJointValue(data.antennas_position[0]);
138
+ }
139
+ if (this.jointMap['right_antenna'] && data.antennas_position[1] !== undefined) {
140
+ this.jointMap['right_antenna'].setJointValue(data.antennas_position[1]);
141
+ }
142
+ }
143
+
144
+ // Update body yaw if available
145
+ if (data.body_yaw !== undefined && this.jointMap['yaw_body']) {
146
+ this.jointMap['yaw_body'].setJointValue(data.body_yaw);
147
+ }
148
+ }
149
+
150
+ getRobot() {
151
+ return this.robot;
152
+ }
153
+ }
reachy_mini_app_example/{robot_state.js → src/RobotState.js} RENAMED
File without changes
reachy_mini_app_example/src/SceneManager.js ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
+
4
+ export class SceneManager {
5
+ constructor(container) {
6
+ this.container = container;
7
+ this.scene = null;
8
+ this.camera = null;
9
+ this.renderer = null;
10
+ this.controls = null;
11
+
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ // Scene setup
17
+ this.scene = new THREE.Scene();
18
+
19
+ // Create retro-futuristic Arizona sunset sky
20
+ this.createSkyBackground();
21
+
22
+ // Camera setup
23
+ this.camera = new THREE.PerspectiveCamera(
24
+ 75,
25
+ window.innerWidth / window.innerHeight,
26
+ 0.01,
27
+ 100
28
+ );
29
+ this.camera.position.set(0.3, 0.3, 0.3);
30
+ this.camera.lookAt(0, 0.1, 0);
31
+
32
+ // Renderer setup
33
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
34
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
35
+ this.renderer.shadowMap.enabled = true;
36
+ this.container.appendChild(this.renderer.domElement);
37
+
38
+ // Controls
39
+ this.controls = new OrbitControls(this.camera, this.renderer.domElement);
40
+ this.controls.target.set(0, 0.1, 0);
41
+ this.controls.enableDamping = true;
42
+ this.controls.dampingFactor = 0.05;
43
+
44
+ // Lighting
45
+ this.setupLighting();
46
+
47
+ // Grid helper
48
+ const gridHelper = new THREE.GridHelper(1, 10);
49
+ this.scene.add(gridHelper);
50
+
51
+ // Handle window resize
52
+ window.addEventListener('resize', () => this.onWindowResize());
53
+ }
54
+
55
+ createSkyBackground() {
56
+ const bgCanvas = document.createElement('canvas');
57
+ bgCanvas.width = 1024;
58
+ bgCanvas.height = 1024;
59
+ const bgCtx = bgCanvas.getContext('2d');
60
+ const bgGradient = bgCtx.createLinearGradient(0, 0, 0, bgCanvas.height);
61
+ bgGradient.addColorStop(0.0, '#f7a072'); // warm orange
62
+ bgGradient.addColorStop(0.08, '#e97a5b'); // orange-red
63
+ bgGradient.addColorStop(0.18, '#cf4958'); // rose-orange
64
+ bgGradient.addColorStop(0.32, '#8e2a6a'); // purple-rose
65
+ bgGradient.addColorStop(0.55, '#4b136f'); // rich purple-magenta
66
+ bgGradient.addColorStop(0.78, '#1a0b4b'); // deep purple
67
+ bgGradient.addColorStop(1.0, '#0b032d'); // very deep purple
68
+ bgCtx.fillStyle = bgGradient;
69
+ bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
70
+ const bgTexture = new THREE.CanvasTexture(bgCanvas);
71
+ if (THREE?.SRGBColorSpace) {
72
+ bgTexture.colorSpace = THREE.SRGBColorSpace;
73
+ }
74
+ this.scene.background = bgTexture;
75
+ }
76
+
77
+ setupLighting() {
78
+ // Ambient light
79
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
80
+ this.scene.add(ambientLight);
81
+
82
+ // Directional light
83
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
84
+ directionalLight.position.set(1, 2, 1);
85
+ directionalLight.castShadow = true;
86
+ this.scene.add(directionalLight);
87
+
88
+ // Spotlight to illuminate the robot nicely
89
+ const spotLight = new THREE.SpotLight(0xffffff, 1.5);
90
+ spotLight.position.set(0.5, 0.5, 0.5);
91
+ spotLight.angle = Math.PI / 4;
92
+ spotLight.penumbra = 0.3;
93
+ spotLight.decay = 2;
94
+ spotLight.distance = 3;
95
+ spotLight.castShadow = true;
96
+ spotLight.target.position.set(0, 0.1, 0);
97
+ this.scene.add(spotLight);
98
+ this.scene.add(spotLight.target);
99
+ }
100
+
101
+ onWindowResize() {
102
+ this.camera.aspect = window.innerWidth / window.innerHeight;
103
+ this.camera.updateProjectionMatrix();
104
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
105
+ }
106
+
107
+ add(object) {
108
+ this.scene.add(object);
109
+ }
110
+
111
+ render() {
112
+ this.controls.update();
113
+ this.renderer.render(this.scene, this.camera);
114
+ }
115
+
116
+ animate() {
117
+ requestAnimationFrame(() => this.animate());
118
+ this.render();
119
+ }
120
+ }
reachy_mini_app_example/src/UIManager.js ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class UIManager {
2
+ constructor(statusElement, jointsListElement) {
3
+ this.statusEl = statusElement;
4
+ this.jointsListEl = jointsListElement;
5
+
6
+ this.jointColors = {
7
+ 'head yaw': '#ff6384',
8
+ 'head pitch': '#36a2eb',
9
+ 'head roll': '#ffce56',
10
+ 'left antenna': '#4bc0c0',
11
+ 'right antenna': '#9966ff',
12
+ 'body yaw': '#ff9f40'
13
+ };
14
+ }
15
+
16
+ updateStatus(message, type = '') {
17
+ if (type === 'connected') {
18
+ this.statusEl.innerHTML = '<span class="ok"><span class="connection-led"></span> Reachy Mini</span>';
19
+ } else if (type === 'disconnected') {
20
+ this.statusEl.innerHTML = '<span class="err"><span class="connection-led"></span>Disconnected. Reconnecting...</span>';
21
+ } else if (type === 'error') {
22
+ this.statusEl.innerHTML = '<span class="err"><span class="connection-led"></span>WebSocket error</span>';
23
+ } else {
24
+ this.statusEl.innerHTML = message;
25
+ }
26
+ }
27
+
28
+ updateJointsDisplay(data) {
29
+ let html = '';
30
+ const jointValues = {};
31
+
32
+ // Display head pose
33
+ if (data.head_pose) {
34
+ const yaw = data.head_pose.yaw * 180 / Math.PI;
35
+ const pitch = data.head_pose.pitch * 180 / Math.PI;
36
+ const roll = data.head_pose.roll * 180 / Math.PI;
37
+
38
+ html += this.createJointItemHTML('head yaw', yaw);
39
+ html += this.createJointItemHTML('head pitch', pitch);
40
+ html += this.createJointItemHTML('head roll', roll);
41
+
42
+ jointValues['head yaw'] = yaw;
43
+ jointValues['head pitch'] = pitch;
44
+ jointValues['head roll'] = roll;
45
+ }
46
+
47
+ // Display antennas
48
+ if (data.antennas_position && Array.isArray(data.antennas_position)) {
49
+ const leftAntenna = data.antennas_position[0] * 180 / Math.PI;
50
+ const rightAntenna = data.antennas_position[1] * 180 / Math.PI;
51
+
52
+ html += this.createJointItemHTML('left antenna', leftAntenna);
53
+ html += this.createJointItemHTML('right antenna', rightAntenna);
54
+
55
+ jointValues['left antenna'] = leftAntenna;
56
+ jointValues['right antenna'] = rightAntenna;
57
+ }
58
+
59
+ // Display body yaw
60
+ if (data.body_yaw !== undefined) {
61
+ const bodyYaw = data.body_yaw * 180 / Math.PI;
62
+ html += this.createJointItemHTML('body yaw', bodyYaw);
63
+ jointValues['body yaw'] = bodyYaw;
64
+ }
65
+
66
+ this.jointsListEl.innerHTML = html || 'No joint data';
67
+
68
+ return jointValues;
69
+ }
70
+
71
+ createJointItemHTML(jointName, value) {
72
+ const color = this.jointColors[jointName] || '#ffffff';
73
+ return `<div class="joint-item">
74
+ <span class="joint-name">
75
+ <span class="joint-color-dot" style="background-color: ${color}"></span>
76
+ ${jointName}
77
+ </span>
78
+ <span class="joint-value">${value.toFixed(2)}°</span>
79
+ </div>`;
80
+ }
81
+ }
reachy_mini_app_example/src/WebSocketManager.js ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RobotStateClient } from './RobotState.js';
2
+
3
+ export class WebSocketManager {
4
+ constructor(uiManager, robotManager, chartManager) {
5
+ this.uiManager = uiManager;
6
+ this.robotManager = robotManager;
7
+ this.chartManager = chartManager;
8
+
9
+ this.WS_URL = new URLSearchParams(location.search).get("ws")
10
+ || "ws://127.0.0.1:8000/api/state/ws/full";
11
+
12
+ this.robotClient = new RobotStateClient(this.WS_URL);
13
+ }
14
+
15
+ connect() {
16
+ this.robotClient.onStateChange((event, data) => {
17
+ switch (event) {
18
+ case 'connected':
19
+ this.uiManager.updateStatus('', 'connected');
20
+ break;
21
+ case 'disconnected':
22
+ this.uiManager.updateStatus('', 'disconnected');
23
+ break;
24
+ case 'error':
25
+ this.uiManager.updateStatus('', 'error');
26
+ break;
27
+ case 'data':
28
+ this.handleDataUpdate(data);
29
+ break;
30
+ }
31
+ });
32
+
33
+ this.robotClient.connect();
34
+ }
35
+
36
+ handleDataUpdate(data) {
37
+ // Update robot joints
38
+ this.robotManager.updateJoints(data);
39
+
40
+ // Update UI and get joint values
41
+ const jointValues = this.uiManager.updateJointsDisplay(data);
42
+
43
+ // Update chart
44
+ this.chartManager.updateChart(jointValues);
45
+ }
46
+ }