cduss Claude Opus 4.5 commited on
Commit
805c57f
·
1 Parent(s): d1a2d70

Update demo app to use central signaling server

Browse files

- Connect to cduss-reachy-mini-central.hf.space
- Add HuggingFace token authentication
- Save token in localStorage for convenience
- Simplified UI for robot connection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. index.html +97 -61
index.html CHANGED
@@ -52,6 +52,9 @@
52
  width: 100%;
53
  margin-bottom: 10px;
54
  }
 
 
 
55
  button {
56
  background: #00d4ff;
57
  color: #000;
@@ -99,21 +102,27 @@
99
  }
100
  .producer-item:hover { background: #1a4a80; }
101
  .producer-item.selected { border: 2px solid #00d4ff; }
 
 
 
 
 
 
 
102
  </style>
103
  </head>
104
  <body>
105
  <div class="container">
106
  <h1>Reachy Mini WebRTC Dashboard</h1>
107
- <p class="subtitle">Test WebRTC connection to robot</p>
108
 
109
  <div class="grid">
110
  <!-- Connection Panel -->
111
  <div class="card">
112
- <h2>1. Signaling Server</h2>
113
- <label>Robot IP Address:</label>
114
- <input type="text" id="robotIp" value="192.168.1.95" placeholder="192.168.1.95">
115
- <label>Signaling Port:</label>
116
- <input type="text" id="signalingPort" value="8443" placeholder="8443">
117
 
118
  <div class="controls">
119
  <button id="connectBtn" onclick="connectSignaling()">Connect</button>
@@ -125,7 +134,7 @@
125
  <span id="signalingStatus" class="status disconnected">Disconnected</span>
126
  </div>
127
 
128
- <h3 style="margin-top: 20px; font-size: 1em;">Available Producers:</h3>
129
  <div id="producerList" class="producer-list">
130
  <em style="color: #666;">Connect to signaling server first</em>
131
  </div>
@@ -175,12 +184,21 @@
175
  </div>
176
 
177
  <script>
 
 
 
178
  // State
179
  let signalingWs = null;
180
  let peerConnection = null;
181
  let dataChannel = null;
182
  let selectedProducerId = null;
183
- let sessionId = null;
 
 
 
 
 
 
184
 
185
  // Logging
186
  function log(message, type = 'info') {
@@ -199,47 +217,51 @@
199
 
200
  // Signaling
201
  function connectSignaling() {
202
- const ip = document.getElementById('robotIp').value;
203
- const port = document.getElementById('signalingPort').value;
204
- const url = `ws://${ip}:${port}`;
 
 
205
 
206
- log(`Connecting to signaling server: ${url}`);
 
 
 
 
207
  updateSignalingStatus('connecting');
208
 
209
  try {
210
  signalingWs = new WebSocket(url);
211
 
212
  signalingWs.onopen = () => {
213
- log('Signaling WebSocket connected!', 'success');
214
  updateSignalingStatus('connected');
215
  document.getElementById('connectBtn').disabled = true;
216
  document.getElementById('disconnectBtn').disabled = false;
217
-
218
- // Request producer list
219
- setTimeout(() => {
220
- log('Requesting producer list...');
221
- signalingWs.send(JSON.stringify({ type: 'list' }));
222
- }, 500);
223
  };
224
 
225
  signalingWs.onmessage = (event) => {
226
- log(`Signaling message: ${event.data.substring(0, 200)}...`);
227
- handleSignalingMessage(JSON.parse(event.data));
 
228
  };
229
 
230
  signalingWs.onerror = (error) => {
231
- log(`Signaling WebSocket error: ${error.message || 'Unknown error'}`, 'error');
232
  };
233
 
234
  signalingWs.onclose = (event) => {
235
- log(`Signaling WebSocket closed: code=${event.code}, reason=${event.reason}`);
 
 
 
236
  updateSignalingStatus('disconnected');
237
  document.getElementById('connectBtn').disabled = false;
238
  document.getElementById('disconnectBtn').disabled = true;
239
  document.getElementById('startStreamBtn').disabled = true;
240
  };
241
  } catch (e) {
242
- log(`Failed to create WebSocket: ${e.message}`, 'error');
243
  updateSignalingStatus('disconnected');
244
  }
245
  }
@@ -259,17 +281,40 @@
259
  }
260
 
261
  function handleSignalingMessage(msg) {
262
- if (msg.type === 'welcome') {
263
- sessionId = msg.peerId;
264
- log(`Got session ID: ${sessionId}`, 'success');
265
- } else if (msg.type === 'list') {
266
- displayProducers(msg.producers);
267
- } else if (msg.type === 'sessionStarted') {
268
- log(`Session started with peer: ${msg.peerId}`, 'success');
269
- } else if (msg.type === 'peer') {
270
- handlePeerMessage(msg);
271
- } else if (msg.type === 'error') {
272
- log(`Signaling error: ${msg.details}`, 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
  }
275
 
@@ -277,21 +322,21 @@
277
  const container = document.getElementById('producerList');
278
  container.innerHTML = '';
279
 
280
- // GStreamer returns array of {id, meta} objects
281
  if (!producers || !Array.isArray(producers) || producers.length === 0) {
282
- container.innerHTML = '<em style="color: #666;">No producers available</em>';
283
  return;
284
  }
285
 
286
  for (const producer of producers) {
287
  const div = document.createElement('div');
288
  div.className = 'producer-item';
289
- div.innerHTML = `<strong>${producer.meta?.name || 'Unknown'}</strong><br><small>${producer.id}</small>`;
 
290
  div.onclick = () => selectProducer(producer.id, div);
291
  container.appendChild(div);
292
  }
293
 
294
- log(`Found ${producers.length} producer(s)`, 'success');
295
  }
296
 
297
  function selectProducer(peerId, element) {
@@ -299,22 +344,21 @@
299
  element.classList.add('selected');
300
  selectedProducerId = peerId;
301
  document.getElementById('startStreamBtn').disabled = false;
302
- log(`Selected producer: ${peerId}`);
303
  }
304
 
305
  // WebRTC
306
  async function startStream() {
307
  if (!selectedProducerId) {
308
- log('No producer selected', 'error');
309
  return;
310
  }
311
 
312
  log('Creating peer connection...');
313
  updateWebrtcStatus('connecting');
314
 
315
- // LAN connection - no STUN needed for local network
316
  const config = {
317
- iceServers: []
318
  };
319
 
320
  peerConnection = new RTCPeerConnection(config);
@@ -342,7 +386,7 @@
342
  };
343
 
344
  peerConnection.oniceconnectionstatechange = () => {
345
- log(`ICE connection state: ${peerConnection.iceConnectionState}`);
346
  if (peerConnection.iceConnectionState === 'connected' ||
347
  peerConnection.iceConnectionState === 'completed') {
348
  updateWebrtcStatus('connected');
@@ -361,19 +405,17 @@
361
  setupDataChannel(dataChannel);
362
  };
363
 
364
- // GStreamer webrtcsink is the offerer, we are the answerer
365
- // Just request to start a session, then wait for SDP offer
366
- log('Requesting session with producer...');
367
  signalingWs.send(JSON.stringify({
368
  type: 'startSession',
369
  peerId: selectedProducerId
370
  }));
371
- // SDP offer will arrive via handlePeerMessage
372
  }
373
 
374
  async function handlePeerMessage(msg) {
375
  if (!peerConnection) {
376
- log('No peer connection, ignoring message', 'error');
377
  return;
378
  }
379
 
@@ -397,14 +439,10 @@
397
 
398
  if (msg.ice) {
399
  log('Received ICE candidate');
400
- try {
401
- await peerConnection.addIceCandidate(new RTCIceCandidate(msg.ice));
402
- } catch (e) {
403
- log(`Failed to add ICE candidate: ${e.message}`, 'error');
404
- }
405
  }
406
  } catch (e) {
407
- log(`Error handling peer message: ${e.message}`, 'error');
408
  }
409
  }
410
 
@@ -436,8 +474,8 @@
436
  function setupDataChannel(channel) {
437
  channel.onopen = () => log('Data channel opened', 'success');
438
  channel.onclose = () => log('Data channel closed');
439
- channel.onerror = (e) => log(`Data channel error: ${e}`, 'error');
440
- channel.onmessage = (event) => log(`Data channel message: ${event.data}`);
441
  }
442
 
443
  // Head Control
@@ -446,14 +484,12 @@
446
  }
447
 
448
  function createRotationMatrix(yawDeg, pitchDeg) {
449
- // Create a 4x4 transformation matrix from yaw and pitch
450
  const yaw = degToRad(yawDeg);
451
  const pitch = degToRad(pitchDeg);
452
 
453
  const cy = Math.cos(yaw), sy = Math.sin(yaw);
454
  const cp = Math.cos(pitch), sp = Math.sin(pitch);
455
 
456
- // Combined rotation: Rz(yaw) * Ry(pitch)
457
  return [
458
  cy * cp, -sy, cy * sp, 0,
459
  sy * cp, cy, sy * sp, 0,
@@ -475,7 +511,7 @@
475
  const msg = JSON.stringify({ set_target: matrix });
476
 
477
  dataChannel.send(msg);
478
- log(`Sent head pose: yaw=${yaw}, pitch=${pitch}`);
479
  }
480
 
481
  function centerHead() {
@@ -485,7 +521,7 @@
485
  }
486
 
487
  // Init
488
- log('Dashboard loaded. Enter robot IP and connect to signaling server.', 'info');
489
  </script>
490
  </body>
491
  </html>
 
52
  width: 100%;
53
  margin-bottom: 10px;
54
  }
55
+ input[type="password"] {
56
+ font-family: monospace;
57
+ }
58
  button {
59
  background: #00d4ff;
60
  color: #000;
 
102
  }
103
  .producer-item:hover { background: #1a4a80; }
104
  .producer-item.selected { border: 2px solid #00d4ff; }
105
+
106
+ .token-info {
107
+ font-size: 0.85em;
108
+ color: #888;
109
+ margin-top: 5px;
110
+ }
111
+ .token-info a { color: #00d4ff; }
112
  </style>
113
  </head>
114
  <body>
115
  <div class="container">
116
  <h1>Reachy Mini WebRTC Dashboard</h1>
117
+ <p class="subtitle">Connect to your robot via central signaling server</p>
118
 
119
  <div class="grid">
120
  <!-- Connection Panel -->
121
  <div class="card">
122
+ <h2>1. Authentication & Connection</h2>
123
+ <label>HuggingFace Token:</label>
124
+ <input type="password" id="hfToken" placeholder="hf_...">
125
+ <p class="token-info">Get your token from <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a></p>
 
126
 
127
  <div class="controls">
128
  <button id="connectBtn" onclick="connectSignaling()">Connect</button>
 
134
  <span id="signalingStatus" class="status disconnected">Disconnected</span>
135
  </div>
136
 
137
+ <h3 style="margin-top: 20px; font-size: 1em;">Available Robots:</h3>
138
  <div id="producerList" class="producer-list">
139
  <em style="color: #666;">Connect to signaling server first</em>
140
  </div>
 
184
  </div>
185
 
186
  <script>
187
+ // Central signaling server
188
+ const SIGNALING_SERVER = 'wss://cduss-reachy-mini-central.hf.space/ws';
189
+
190
  // State
191
  let signalingWs = null;
192
  let peerConnection = null;
193
  let dataChannel = null;
194
  let selectedProducerId = null;
195
+ let myPeerId = null;
196
+
197
+ // Try to load token from localStorage
198
+ const savedToken = localStorage.getItem('hf_token');
199
+ if (savedToken) {
200
+ document.getElementById('hfToken').value = savedToken;
201
+ }
202
 
203
  // Logging
204
  function log(message, type = 'info') {
 
217
 
218
  // Signaling
219
  function connectSignaling() {
220
+ const token = document.getElementById('hfToken').value.trim();
221
+ if (!token) {
222
+ log('Please enter your HuggingFace token', 'error');
223
+ return;
224
+ }
225
 
226
+ // Save token for convenience
227
+ localStorage.setItem('hf_token', token);
228
+
229
+ const url = `${SIGNALING_SERVER}?token=${encodeURIComponent(token)}`;
230
+ log(`Connecting to central signaling server...`);
231
  updateSignalingStatus('connecting');
232
 
233
  try {
234
  signalingWs = new WebSocket(url);
235
 
236
  signalingWs.onopen = () => {
237
+ log('Connected to signaling server!', 'success');
238
  updateSignalingStatus('connected');
239
  document.getElementById('connectBtn').disabled = true;
240
  document.getElementById('disconnectBtn').disabled = false;
 
 
 
 
 
 
241
  };
242
 
243
  signalingWs.onmessage = (event) => {
244
+ const msg = JSON.parse(event.data);
245
+ log(`Received: ${msg.type}`);
246
+ handleSignalingMessage(msg);
247
  };
248
 
249
  signalingWs.onerror = (error) => {
250
+ log(`WebSocket error`, 'error');
251
  };
252
 
253
  signalingWs.onclose = (event) => {
254
+ log(`Disconnected: code=${event.code}`);
255
+ if (event.code === 4001) {
256
+ log('Invalid token - please check your HuggingFace token', 'error');
257
+ }
258
  updateSignalingStatus('disconnected');
259
  document.getElementById('connectBtn').disabled = false;
260
  document.getElementById('disconnectBtn').disabled = true;
261
  document.getElementById('startStreamBtn').disabled = true;
262
  };
263
  } catch (e) {
264
+ log(`Failed to connect: ${e.message}`, 'error');
265
  updateSignalingStatus('disconnected');
266
  }
267
  }
 
281
  }
282
 
283
  function handleSignalingMessage(msg) {
284
+ switch (msg.type) {
285
+ case 'welcome':
286
+ myPeerId = msg.peerId;
287
+ log(`My peer ID: ${myPeerId}`, 'success');
288
+ // Request producer list
289
+ signalingWs.send(JSON.stringify({ type: 'list' }));
290
+ break;
291
+
292
+ case 'list':
293
+ displayProducers(msg.producers);
294
+ break;
295
+
296
+ case 'peerStatusChanged':
297
+ log(`Producer ${msg.peerId} ${msg.roles?.length ? 'available' : 'left'}`);
298
+ // Refresh list
299
+ signalingWs.send(JSON.stringify({ type: 'list' }));
300
+ break;
301
+
302
+ case 'sessionStarted':
303
+ log(`Session started with producer: ${msg.peerId}`, 'success');
304
+ break;
305
+
306
+ case 'peer':
307
+ handlePeerMessage(msg);
308
+ break;
309
+
310
+ case 'endSession':
311
+ log('Session ended');
312
+ stopStream();
313
+ break;
314
+
315
+ case 'error':
316
+ log(`Server error: ${msg.details}`, 'error');
317
+ break;
318
  }
319
  }
320
 
 
322
  const container = document.getElementById('producerList');
323
  container.innerHTML = '';
324
 
 
325
  if (!producers || !Array.isArray(producers) || producers.length === 0) {
326
+ container.innerHTML = '<em style="color: #666;">No robots available. Make sure your robot is connected.</em>';
327
  return;
328
  }
329
 
330
  for (const producer of producers) {
331
  const div = document.createElement('div');
332
  div.className = 'producer-item';
333
+ const name = producer.meta?.name || 'Unknown Robot';
334
+ div.innerHTML = `<strong>${name}</strong><br><small style="color: #888;">${producer.id.substring(0, 8)}...</small>`;
335
  div.onclick = () => selectProducer(producer.id, div);
336
  container.appendChild(div);
337
  }
338
 
339
+ log(`Found ${producers.length} robot(s)`, 'success');
340
  }
341
 
342
  function selectProducer(peerId, element) {
 
344
  element.classList.add('selected');
345
  selectedProducerId = peerId;
346
  document.getElementById('startStreamBtn').disabled = false;
347
+ log(`Selected robot: ${peerId.substring(0, 8)}...`);
348
  }
349
 
350
  // WebRTC
351
  async function startStream() {
352
  if (!selectedProducerId) {
353
+ log('No robot selected', 'error');
354
  return;
355
  }
356
 
357
  log('Creating peer connection...');
358
  updateWebrtcStatus('connecting');
359
 
 
360
  const config = {
361
+ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
362
  };
363
 
364
  peerConnection = new RTCPeerConnection(config);
 
386
  };
387
 
388
  peerConnection.oniceconnectionstatechange = () => {
389
+ log(`ICE state: ${peerConnection.iceConnectionState}`);
390
  if (peerConnection.iceConnectionState === 'connected' ||
391
  peerConnection.iceConnectionState === 'completed') {
392
  updateWebrtcStatus('connected');
 
405
  setupDataChannel(dataChannel);
406
  };
407
 
408
+ // Request session with producer - producer will send the SDP offer
409
+ log('Requesting session with robot...');
 
410
  signalingWs.send(JSON.stringify({
411
  type: 'startSession',
412
  peerId: selectedProducerId
413
  }));
 
414
  }
415
 
416
  async function handlePeerMessage(msg) {
417
  if (!peerConnection) {
418
+ log('No peer connection', 'error');
419
  return;
420
  }
421
 
 
439
 
440
  if (msg.ice) {
441
  log('Received ICE candidate');
442
+ await peerConnection.addIceCandidate(new RTCIceCandidate(msg.ice));
 
 
 
 
443
  }
444
  } catch (e) {
445
+ log(`Error: ${e.message}`, 'error');
446
  }
447
  }
448
 
 
474
  function setupDataChannel(channel) {
475
  channel.onopen = () => log('Data channel opened', 'success');
476
  channel.onclose = () => log('Data channel closed');
477
+ channel.onerror = (e) => log(`Data channel error`, 'error');
478
+ channel.onmessage = (event) => log(`Received: ${event.data}`);
479
  }
480
 
481
  // Head Control
 
484
  }
485
 
486
  function createRotationMatrix(yawDeg, pitchDeg) {
 
487
  const yaw = degToRad(yawDeg);
488
  const pitch = degToRad(pitchDeg);
489
 
490
  const cy = Math.cos(yaw), sy = Math.sin(yaw);
491
  const cp = Math.cos(pitch), sp = Math.sin(pitch);
492
 
 
493
  return [
494
  cy * cp, -sy, cy * sp, 0,
495
  sy * cp, cy, sy * sp, 0,
 
511
  const msg = JSON.stringify({ set_target: matrix });
512
 
513
  dataChannel.send(msg);
514
+ log(`Sent pose: yaw=${yaw}, pitch=${pitch}`);
515
  }
516
 
517
  function centerHead() {
 
521
  }
522
 
523
  // Init
524
+ log('Ready. Enter your HuggingFace token and connect.', 'info');
525
  </script>
526
  </body>
527
  </html>