Spaces:
Running
Running
Add full robot control panel via WebRTC
Browse filesNew 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>
- 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 193 |
-
<button id="disableMotorsBtn" onclick="setMotorMode('disabled')" disabled style="background: #ff5252;">Disable
|
|
|
|
| 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 |
-
<
|
| 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
|
| 213 |
-
<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 |
-
|
| 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
|
| 599 |
-
dataChannel.send(JSON.stringify({
|
| 600 |
};
|
| 601 |
dataChannel.onclose = () => log('Data channel closed');
|
| 602 |
dataChannel.onmessage = (e) => {
|
| 603 |
try {
|
| 604 |
const data = JSON.parse(e.data);
|
| 605 |
-
|
| 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 |
-
|
| 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 |
-
//
|
| 688 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
if (!dataChannel || dataChannel.readyState !== 'open') {
|
| 690 |
log('Data channel not ready', 'error');
|
| 691 |
-
return;
|
| 692 |
}
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
const yawRad =
|
| 697 |
-
const pitchRad =
|
| 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 |
-
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
}
|
| 713 |
|
| 714 |
function centerHead() {
|
| 715 |
document.getElementById('yawInput').value = 0;
|
| 716 |
document.getElementById('pitchInput').value = 0;
|
|
|
|
| 717 |
sendHeadPose();
|
| 718 |
}
|
| 719 |
|
| 720 |
-
//
|
| 721 |
-
function
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
}
|
|
|
|
| 726 |
|
| 727 |
-
|
| 728 |
-
|
|
|
|
|
|
|
| 729 |
}
|
| 730 |
|
| 731 |
function updateMotorStatus(mode) {
|
| 732 |
const el = document.getElementById('motorStatus');
|
| 733 |
if (!el) return;
|
| 734 |
-
|
|
|
|
| 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>
|