cduss Claude Opus 4.5 commited on
Commit
9dcd4eb
·
1 Parent(s): 0c2e821

Add full robot control panel via WebRTC

Browse files

New controls:
- Motor: enable/disable/gravity compensation
- Head: yaw/pitch with smooth goto option
- Body yaw rotation
- Antennas: sliders for right/left
- Animations: wake up / go to sleep
- Audio: play sound files
- Recording: start/stop movement recording
- State: refresh current robot state

All commands sent via WebRTC data channel for low latency.

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

Files changed (1) hide show
  1. index.html +274 -50
index.html CHANGED
@@ -95,7 +95,30 @@
95
  }
96
 
97
  .controls { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px; }
98
- .controls button { flex: 1; min-width: 120px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  .producer-list { margin: 10px 0; }
101
  .producer-item {
@@ -185,20 +208,26 @@
185
 
186
  <!-- Control Panel -->
187
  <div class="card">
188
- <h2>3. Motor & Head Control</h2>
189
 
190
  <p style="color: #888; font-size: 0.9em; margin-bottom: 10px;">Motor control (must enable before moving)</p>
191
  <div class="controls" style="margin-bottom: 15px;">
192
- <button id="enableMotorsBtn" onclick="setMotorMode('enabled')" disabled style="background: #00c853;">Enable Motors</button>
193
- <button id="disableMotorsBtn" onclick="setMotorMode('disabled')" disabled style="background: #ff5252;">Disable Motors</button>
 
194
  </div>
195
  <div style="margin-bottom: 15px;">
196
  <span>Motors: </span>
197
  <span id="motorStatus" class="status disconnected">Unknown</span>
 
198
  </div>
 
 
 
 
 
199
 
200
- <p style="color: #888; font-size: 0.9em;">Head pose commands</p>
201
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
202
  <div>
203
  <label>Yaw (deg):</label>
204
  <input type="number" id="yawInput" value="0" min="-45" max="45">
@@ -207,10 +236,77 @@
207
  <label>Pitch (deg):</label>
208
  <input type="number" id="pitchInput" value="0" min="-30" max="30">
209
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  </div>
 
211
  <div class="controls">
212
- <button id="sendPoseBtn" onclick="sendHeadPose()" disabled>Send Pose</button>
213
- <button id="centerBtn" onclick="centerHead()" disabled>Center</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </div>
215
  </div>
216
 
@@ -249,7 +345,16 @@
249
  window.stopStream = stopStream;
250
  window.sendHeadPose = sendHeadPose;
251
  window.centerHead = centerHead;
 
252
  window.setMotorMode = setMotorMode;
 
 
 
 
 
 
 
 
253
  window.clearLog = clearLog;
254
 
255
  // Initialize on page load
@@ -579,11 +684,7 @@
579
  if (peerConnection.iceConnectionState === 'connected' ||
580
  peerConnection.iceConnectionState === 'completed') {
581
  updateWebrtcStatus('connected');
582
- document.getElementById('stopStreamBtn').disabled = false;
583
- document.getElementById('sendPoseBtn').disabled = false;
584
- document.getElementById('centerBtn').disabled = false;
585
- document.getElementById('enableMotorsBtn').disabled = false;
586
- document.getElementById('disableMotorsBtn').disabled = false;
587
  } else if (peerConnection.iceConnectionState === 'failed') {
588
  updateWebrtcStatus('disconnected');
589
  log('Connection failed', 'error');
@@ -595,21 +696,14 @@
595
  dataChannel = event.channel;
596
  dataChannel.onopen = () => {
597
  log('Data channel open', 'success');
598
- // Query motor status when channel opens
599
- dataChannel.send(JSON.stringify({ get_motor_mode: true }));
600
  };
601
  dataChannel.onclose = () => log('Data channel closed');
602
  dataChannel.onmessage = (e) => {
603
  try {
604
  const data = JSON.parse(e.data);
605
- if (data.motor_mode) {
606
- updateMotorStatus(data.motor_mode);
607
- log(`Motor mode: ${data.motor_mode}`, 'info');
608
- } else if (data.error) {
609
- log(`Error: ${data.error}`, 'error');
610
- } else {
611
- log(`Received: ${e.data}`);
612
- }
613
  } catch {
614
  log(`Received: ${e.data}`);
615
  }
@@ -668,12 +762,21 @@
668
  currentSessionId = null;
669
  document.getElementById('remoteVideo').srcObject = null;
670
  updateWebrtcStatus('disconnected');
671
- document.getElementById('stopStreamBtn').disabled = true;
672
- document.getElementById('sendPoseBtn').disabled = true;
673
- document.getElementById('centerBtn').disabled = true;
674
- document.getElementById('enableMotorsBtn').disabled = true;
675
- document.getElementById('disableMotorsBtn').disabled = true;
676
  updateMotorStatus('unknown');
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  }
678
 
679
  function updateWebrtcStatus(status) {
@@ -684,54 +787,105 @@
684
  el.textContent = labels[status] || status;
685
  }
686
 
687
- // Head Control
688
- function sendHeadPose() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  if (!dataChannel || dataChannel.readyState !== 'open') {
690
  log('Data channel not ready', 'error');
691
- return;
692
  }
 
 
 
693
 
694
- const yaw = parseFloat(document.getElementById('yawInput').value) || 0;
695
- const pitch = parseFloat(document.getElementById('pitchInput').value) || 0;
696
- const yawRad = yaw * Math.PI / 180;
697
- const pitchRad = pitch * Math.PI / 180;
698
-
699
  const cy = Math.cos(yawRad), sy = Math.sin(yawRad);
700
  const cp = Math.cos(pitchRad), sp = Math.sin(pitchRad);
701
-
702
- // 4x4 transformation matrix (nested array for numpy compatibility)
703
- const matrix = [
704
  [cy * cp, -sy, cy * sp, 0],
705
  [sy * cp, cy, sy * sp, 0],
706
  [-sp, 0, cp, 0],
707
  [0, 0, 0, 1]
708
  ];
 
709
 
710
- dataChannel.send(JSON.stringify({ set_target: matrix }));
711
- log(`Sent pose: yaw=${yaw}, pitch=${pitch}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  }
713
 
714
  function centerHead() {
715
  document.getElementById('yawInput').value = 0;
716
  document.getElementById('pitchInput').value = 0;
 
717
  sendHeadPose();
718
  }
719
 
720
- // Motor Control
721
- function setMotorMode(mode) {
722
- if (!dataChannel || dataChannel.readyState !== 'open') {
723
- log('Data channel not ready', 'error');
724
- return;
725
- }
 
726
 
727
- dataChannel.send(JSON.stringify({ set_motor_mode: mode }));
728
- log(`Setting motor mode to: ${mode}`);
 
 
729
  }
730
 
731
  function updateMotorStatus(mode) {
732
  const el = document.getElementById('motorStatus');
733
  if (!el) return;
734
- el.textContent = mode.charAt(0).toUpperCase() + mode.slice(1);
 
735
  if (mode === 'enabled') {
736
  el.className = 'status connected';
737
  } else if (mode === 'disabled') {
@@ -740,6 +894,76 @@
740
  el.className = 'status connecting';
741
  }
742
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  </script>
744
  </body>
745
  </html>
 
95
  }
96
 
97
  .controls { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px; }
98
+ .controls button { flex: 1; min-width: 100px; }
99
+ .control-btn { min-width: 80px; }
100
+
101
+ input[type="range"] {
102
+ -webkit-appearance: none;
103
+ height: 8px;
104
+ background: #0f3460;
105
+ border-radius: 4px;
106
+ margin-top: 8px;
107
+ }
108
+ input[type="range"]::-webkit-slider-thumb {
109
+ -webkit-appearance: none;
110
+ width: 18px;
111
+ height: 18px;
112
+ background: #00d4ff;
113
+ border-radius: 50%;
114
+ cursor: pointer;
115
+ }
116
+
117
+ input[type="checkbox"] {
118
+ width: 18px;
119
+ height: 18px;
120
+ vertical-align: middle;
121
+ }
122
 
123
  .producer-list { margin: 10px 0; }
124
  .producer-item {
 
208
 
209
  <!-- Control Panel -->
210
  <div class="card">
211
+ <h2>3. Motor Control</h2>
212
 
213
  <p style="color: #888; font-size: 0.9em; margin-bottom: 10px;">Motor control (must enable before moving)</p>
214
  <div class="controls" style="margin-bottom: 15px;">
215
+ <button id="enableMotorsBtn" onclick="setMotorMode('enabled')" disabled style="background: #00c853;">Enable</button>
216
+ <button id="disableMotorsBtn" onclick="setMotorMode('disabled')" disabled style="background: #ff5252;">Disable</button>
217
+ <button id="gravityBtn" onclick="setMotorMode('gravity_compensation')" disabled style="background: #ffc107; color: #000;">Gravity Comp</button>
218
  </div>
219
  <div style="margin-bottom: 15px;">
220
  <span>Motors: </span>
221
  <span id="motorStatus" class="status disconnected">Unknown</span>
222
+ <button onclick="getState()" style="margin-left: 10px; padding: 4px 8px; font-size: 0.8em;">Refresh State</button>
223
  </div>
224
+ </div>
225
+
226
+ <!-- Head & Body Control -->
227
+ <div class="card">
228
+ <h2>4. Head & Body Control</h2>
229
 
230
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 15px;">
 
231
  <div>
232
  <label>Yaw (deg):</label>
233
  <input type="number" id="yawInput" value="0" min="-45" max="45">
 
236
  <label>Pitch (deg):</label>
237
  <input type="number" id="pitchInput" value="0" min="-30" max="30">
238
  </div>
239
+ <div>
240
+ <label>Body Yaw (deg):</label>
241
+ <input type="number" id="bodyYawInput" value="0" min="-45" max="45">
242
+ </div>
243
+ </div>
244
+
245
+ <div style="margin-bottom: 15px;">
246
+ <label>Duration (s): </label>
247
+ <input type="number" id="durationInput" value="0.5" min="0.1" max="5" step="0.1" style="width: 80px;">
248
+ <label style="margin-left: 10px;">
249
+ <input type="checkbox" id="smoothMoveCheck"> Smooth move (goto)
250
+ </label>
251
+ </div>
252
+
253
+ <div class="controls">
254
+ <button class="control-btn" onclick="sendHeadPose()" disabled>Send Pose</button>
255
+ <button class="control-btn" onclick="centerHead()" disabled>Center</button>
256
+ <button class="control-btn" onclick="setBodyYaw()" disabled>Set Body Yaw</button>
257
+ </div>
258
+ </div>
259
+
260
+ <!-- Antennas Control -->
261
+ <div class="card">
262
+ <h2>5. Antennas</h2>
263
+
264
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
265
+ <div>
266
+ <label>Right Antenna: <span id="rightAntennaVal">0</span>°</label>
267
+ <input type="range" id="rightAntennaInput" value="0" min="-175" max="175" style="width: 100%;" oninput="document.getElementById('rightAntennaVal').textContent = this.value">
268
+ </div>
269
+ <div>
270
+ <label>Left Antenna: <span id="leftAntennaVal">0</span>°</label>
271
+ <input type="range" id="leftAntennaInput" value="0" min="-175" max="175" style="width: 100%;" oninput="document.getElementById('leftAntennaVal').textContent = this.value">
272
+ </div>
273
  </div>
274
+
275
  <div class="controls">
276
+ <button class="control-btn" onclick="sendAntennas()" disabled>Set Antennas</button>
277
+ <button class="control-btn" onclick="resetAntennas()" disabled>Reset (0°)</button>
278
+ </div>
279
+ </div>
280
+
281
+ <!-- Animations & Audio -->
282
+ <div class="card">
283
+ <h2>6. Animations & Audio</h2>
284
+
285
+ <p style="color: #888; font-size: 0.9em; margin-bottom: 10px;">Pre-built animations</p>
286
+ <div class="controls" style="margin-bottom: 15px;">
287
+ <button class="control-btn" onclick="wakeUp()" disabled style="background: #00c853;">Wake Up</button>
288
+ <button class="control-btn" onclick="goToSleep()" disabled style="background: #9c27b0;">Go to Sleep</button>
289
+ </div>
290
+
291
+ <p style="color: #888; font-size: 0.9em; margin-bottom: 10px;">Play sound</p>
292
+ <div style="display: flex; gap: 10px;">
293
+ <input type="text" id="soundFileInput" placeholder="Sound file (e.g., wake_up.wav)" style="flex: 1;">
294
+ <button class="control-btn" onclick="playSound()" disabled>Play</button>
295
+ </div>
296
+ </div>
297
+
298
+ <!-- Recording -->
299
+ <div class="card">
300
+ <h2>7. Recording</h2>
301
+
302
+ <div style="margin-bottom: 15px;">
303
+ <span>Recording: </span>
304
+ <span id="recordingStatus" class="status disconnected">Stopped</span>
305
+ </div>
306
+
307
+ <div class="controls">
308
+ <button class="control-btn" onclick="startRecording()" disabled style="background: #f44336;">Start Recording</button>
309
+ <button class="control-btn" onclick="stopRecording()" disabled>Stop Recording</button>
310
  </div>
311
  </div>
312
 
 
345
  window.stopStream = stopStream;
346
  window.sendHeadPose = sendHeadPose;
347
  window.centerHead = centerHead;
348
+ window.setBodyYaw = setBodyYaw;
349
  window.setMotorMode = setMotorMode;
350
+ window.sendAntennas = sendAntennas;
351
+ window.resetAntennas = resetAntennas;
352
+ window.wakeUp = wakeUp;
353
+ window.goToSleep = goToSleep;
354
+ window.playSound = playSound;
355
+ window.getState = getState;
356
+ window.startRecording = startRecording;
357
+ window.stopRecording = stopRecording;
358
  window.clearLog = clearLog;
359
 
360
  // Initialize on page load
 
684
  if (peerConnection.iceConnectionState === 'connected' ||
685
  peerConnection.iceConnectionState === 'completed') {
686
  updateWebrtcStatus('connected');
687
+ enableAllControls(true);
 
 
 
 
688
  } else if (peerConnection.iceConnectionState === 'failed') {
689
  updateWebrtcStatus('disconnected');
690
  log('Connection failed', 'error');
 
696
  dataChannel = event.channel;
697
  dataChannel.onopen = () => {
698
  log('Data channel open', 'success');
699
+ // Query full state when channel opens
700
+ dataChannel.send(JSON.stringify({ get_state: true }));
701
  };
702
  dataChannel.onclose = () => log('Data channel closed');
703
  dataChannel.onmessage = (e) => {
704
  try {
705
  const data = JSON.parse(e.data);
706
+ handleRobotResponse(data);
 
 
 
 
 
 
 
707
  } catch {
708
  log(`Received: ${e.data}`);
709
  }
 
762
  currentSessionId = null;
763
  document.getElementById('remoteVideo').srcObject = null;
764
  updateWebrtcStatus('disconnected');
765
+ enableAllControls(false);
 
 
 
 
766
  updateMotorStatus('unknown');
767
+ updateRecordingStatus(false);
768
+ }
769
+
770
+ function enableAllControls(enabled) {
771
+ // All control buttons
772
+ const buttons = document.querySelectorAll('.control-btn');
773
+ buttons.forEach(btn => btn.disabled = !enabled);
774
+
775
+ // Specific buttons
776
+ document.getElementById('stopStreamBtn').disabled = !enabled;
777
+ document.getElementById('enableMotorsBtn').disabled = !enabled;
778
+ document.getElementById('disableMotorsBtn').disabled = !enabled;
779
+ document.getElementById('gravityBtn').disabled = !enabled;
780
  }
781
 
782
  function updateWebrtcStatus(status) {
 
787
  el.textContent = labels[status] || status;
788
  }
789
 
790
+ // === RESPONSE HANDLER ===
791
+ function handleRobotResponse(data) {
792
+ if (data.state) {
793
+ // Full state response
794
+ if (data.state.motor_mode) updateMotorStatus(data.state.motor_mode);
795
+ if (data.state.is_recording !== undefined) updateRecordingStatus(data.state.is_recording);
796
+ log(`State: motors=${data.state.motor_mode}, recording=${data.state.is_recording}`, 'info');
797
+ } else if (data.motor_mode) {
798
+ updateMotorStatus(data.motor_mode);
799
+ log(`Motor mode: ${data.motor_mode}`, 'info');
800
+ } else if (data.error) {
801
+ log(`Error: ${data.error}`, 'error');
802
+ } else if (data.status === 'ok') {
803
+ const cmd = data.command || 'command';
804
+ if (data.completed) {
805
+ log(`${cmd} completed`, 'success');
806
+ } else {
807
+ log(`${cmd} sent`, 'info');
808
+ }
809
+ if (data.is_recording !== undefined) updateRecordingStatus(data.is_recording);
810
+ } else {
811
+ log(`Response: ${JSON.stringify(data)}`);
812
+ }
813
+ }
814
+
815
+ // === HELPER: Send command via data channel ===
816
+ function sendCommand(cmd) {
817
  if (!dataChannel || dataChannel.readyState !== 'open') {
818
  log('Data channel not ready', 'error');
819
+ return false;
820
  }
821
+ dataChannel.send(JSON.stringify(cmd));
822
+ return true;
823
+ }
824
 
825
+ // === HEAD CONTROL ===
826
+ function buildPoseMatrix(yawDeg, pitchDeg) {
827
+ const yawRad = yawDeg * Math.PI / 180;
828
+ const pitchRad = pitchDeg * Math.PI / 180;
 
829
  const cy = Math.cos(yawRad), sy = Math.sin(yawRad);
830
  const cp = Math.cos(pitchRad), sp = Math.sin(pitchRad);
831
+ return [
 
 
832
  [cy * cp, -sy, cy * sp, 0],
833
  [sy * cp, cy, sy * sp, 0],
834
  [-sp, 0, cp, 0],
835
  [0, 0, 0, 1]
836
  ];
837
+ }
838
 
839
+ function sendHeadPose() {
840
+ const yaw = parseFloat(document.getElementById('yawInput').value) || 0;
841
+ const pitch = parseFloat(document.getElementById('pitchInput').value) || 0;
842
+ const smooth = document.getElementById('smoothMoveCheck').checked;
843
+ const duration = parseFloat(document.getElementById('durationInput').value) || 0.5;
844
+
845
+ const matrix = buildPoseMatrix(yaw, pitch);
846
+
847
+ if (smooth) {
848
+ // Smooth goto movement
849
+ sendCommand({
850
+ goto_target: {
851
+ head: matrix,
852
+ duration: duration
853
+ }
854
+ });
855
+ log(`Goto pose: yaw=${yaw}, pitch=${pitch}, duration=${duration}s`);
856
+ } else {
857
+ // Instant set_target
858
+ sendCommand({ set_target: matrix });
859
+ log(`Set pose: yaw=${yaw}, pitch=${pitch}`);
860
+ }
861
  }
862
 
863
  function centerHead() {
864
  document.getElementById('yawInput').value = 0;
865
  document.getElementById('pitchInput').value = 0;
866
+ document.getElementById('bodyYawInput').value = 0;
867
  sendHeadPose();
868
  }
869
 
870
+ // === BODY YAW ===
871
+ function setBodyYaw() {
872
+ const yaw = parseFloat(document.getElementById('bodyYawInput').value) || 0;
873
+ const yawRad = yaw * Math.PI / 180;
874
+ sendCommand({ set_body_yaw: yawRad });
875
+ log(`Set body yaw: ${yaw}°`);
876
+ }
877
 
878
+ // === MOTOR CONTROL ===
879
+ function setMotorMode(mode) {
880
+ sendCommand({ set_motor_mode: mode });
881
+ log(`Setting motor mode: ${mode}`);
882
  }
883
 
884
  function updateMotorStatus(mode) {
885
  const el = document.getElementById('motorStatus');
886
  if (!el) return;
887
+ const label = mode.replace('_', ' ');
888
+ el.textContent = label.charAt(0).toUpperCase() + label.slice(1);
889
  if (mode === 'enabled') {
890
  el.className = 'status connected';
891
  } else if (mode === 'disabled') {
 
894
  el.className = 'status connecting';
895
  }
896
  }
897
+
898
+ // === ANTENNAS ===
899
+ function sendAntennas() {
900
+ const right = parseFloat(document.getElementById('rightAntennaInput').value) || 0;
901
+ const left = parseFloat(document.getElementById('leftAntennaInput').value) || 0;
902
+ const rightRad = right * Math.PI / 180;
903
+ const leftRad = left * Math.PI / 180;
904
+ sendCommand({ set_antennas: [rightRad, leftRad] });
905
+ log(`Set antennas: right=${right}°, left=${left}°`);
906
+ }
907
+
908
+ function resetAntennas() {
909
+ document.getElementById('rightAntennaInput').value = 0;
910
+ document.getElementById('leftAntennaInput').value = 0;
911
+ document.getElementById('rightAntennaVal').textContent = '0';
912
+ document.getElementById('leftAntennaVal').textContent = '0';
913
+ sendCommand({ set_antennas: [0, 0] });
914
+ log('Reset antennas to 0°');
915
+ }
916
+
917
+ // === ANIMATIONS ===
918
+ function wakeUp() {
919
+ sendCommand({ wake_up: true });
920
+ log('Starting wake up animation...', 'info');
921
+ }
922
+
923
+ function goToSleep() {
924
+ sendCommand({ goto_sleep: true });
925
+ log('Starting sleep animation...', 'info');
926
+ }
927
+
928
+ // === AUDIO ===
929
+ function playSound() {
930
+ const file = document.getElementById('soundFileInput').value.trim();
931
+ if (!file) {
932
+ log('Please enter a sound file name', 'error');
933
+ return;
934
+ }
935
+ sendCommand({ play_sound: file });
936
+ log(`Playing sound: ${file}`);
937
+ }
938
+
939
+ // === STATE ===
940
+ function getState() {
941
+ sendCommand({ get_state: true });
942
+ log('Requesting state...');
943
+ }
944
+
945
+ // === RECORDING ===
946
+ function startRecording() {
947
+ sendCommand({ start_recording: true });
948
+ log('Starting recording...', 'info');
949
+ }
950
+
951
+ function stopRecording() {
952
+ sendCommand({ stop_recording: true });
953
+ log('Stopping recording...', 'info');
954
+ }
955
+
956
+ function updateRecordingStatus(isRecording) {
957
+ const el = document.getElementById('recordingStatus');
958
+ if (!el) return;
959
+ if (isRecording) {
960
+ el.textContent = 'Recording';
961
+ el.className = 'status connected';
962
+ } else {
963
+ el.textContent = 'Stopped';
964
+ el.className = 'status disconnected';
965
+ }
966
+ }
967
  </script>
968
  </body>
969
  </html>