// Reachy Mini Control Panel - WebSocket Version // Connects to localhost:8000 WebSocket API for real-time control const ROBOT_URL = 'localhost:8000'; const WS_URL = `ws://${ROBOT_URL}/api/move/ws/set_target`; // Global state const state = { ws: null, connected: false, isRefreshing: false, currentPose: { head: { x: 0, y: 0, z: 0, roll: 0, pitch: 0, yaw: 0 }, bodyYaw: 0, antennas: [0, 0] } }; // DOM elements const elements = { status: document.getElementById('connectionStatus'), sliders: { headX: document.getElementById('headX'), headY: document.getElementById('headY'), headZ: document.getElementById('headZ'), headRoll: document.getElementById('headRoll'), headPitch: document.getElementById('headPitch'), headYaw: document.getElementById('headYaw'), bodyYaw: document.getElementById('bodyYaw'), antennaLeft: document.getElementById('antennaLeft'), antennaRight: document.getElementById('antennaRight') }, values: { headX: document.getElementById('headXValue'), headY: document.getElementById('headYValue'), headZ: document.getElementById('headZValue'), headRoll: document.getElementById('headRollValue'), headPitch: document.getElementById('headPitchValue'), headYaw: document.getElementById('headYawValue'), bodyYaw: document.getElementById('bodyYawValue'), antennaLeft: document.getElementById('antennaLeftValue'), antennaRight: document.getElementById('antennaRightValue') } }; // WebSocket connection function connectWebSocket() { console.log('Connecting to WebSocket:', WS_URL); state.ws = new WebSocket(WS_URL); state.ws.onopen = () => { console.log('WebSocket connected'); state.connected = true; updateConnectionStatus(true); enableControls(true); }; state.ws.onclose = () => { console.log('WebSocket disconnected'); state.connected = false; updateConnectionStatus(false); enableControls(false); // Reconnect after 2 seconds setTimeout(connectWebSocket, 2000); }; state.ws.onerror = (error) => { console.error('WebSocket error:', error); }; state.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.status === 'error') { console.error('Server error:', message.detail); } } catch (e) { console.error('Failed to parse message:', e); } }; } // Update connection status UI function updateConnectionStatus(connected) { if (connected) { elements.status.className = 'status connected'; elements.status.innerHTML = 'Connected to robot'; } else { elements.status.className = 'status disconnected'; elements.status.innerHTML = 'Disconnected - Reconnecting...'; } } // Enable/disable controls function enableControls(enabled) { Object.values(elements.sliders).forEach(slider => { slider.disabled = !enabled; }); } // Send target pose via WebSocket function sendTargetPose() { if (!state.connected || !state.ws || state.ws.readyState !== WebSocket.OPEN) { console.warn('WebSocket not connected'); return; } if (state.isRefreshing) { return; // Don't send during refresh } const message = { target_head_pose: { x: state.currentPose.head.x, y: state.currentPose.head.y, z: state.currentPose.head.z, roll: state.currentPose.head.roll, pitch: state.currentPose.head.pitch, yaw: state.currentPose.head.yaw }, target_body_yaw: state.currentPose.bodyYaw, target_antennas: state.currentPose.antennas }; try { state.ws.send(JSON.stringify(message)); } catch (error) { console.error('Failed to send message:', error); } } // Slider change handlers function setupSliderHandlers() { // Head pose sliders elements.sliders.headX.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.head.x = value; elements.values.headX.textContent = value.toFixed(3); sendTargetPose(); }); elements.sliders.headY.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.head.y = value; elements.values.headY.textContent = value.toFixed(3); sendTargetPose(); }); elements.sliders.headZ.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.head.z = value; elements.values.headZ.textContent = value.toFixed(3); sendTargetPose(); }); elements.sliders.headRoll.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.head.roll = value; elements.values.headRoll.textContent = value.toFixed(2); sendTargetPose(); }); elements.sliders.headPitch.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.head.pitch = value; elements.values.headPitch.textContent = value.toFixed(2); sendTargetPose(); }); elements.sliders.headYaw.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.head.yaw = value; elements.values.headYaw.textContent = value.toFixed(2); sendTargetPose(); }); // Body yaw slider elements.sliders.bodyYaw.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.bodyYaw = value; elements.values.bodyYaw.textContent = value.toFixed(2); sendTargetPose(); }); // Antenna sliders elements.sliders.antennaLeft.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.antennas[0] = value; elements.values.antennaLeft.textContent = value.toFixed(2); sendTargetPose(); }); elements.sliders.antennaRight.addEventListener('input', (e) => { const value = parseFloat(e.target.value); state.currentPose.antennas[1] = value; elements.values.antennaRight.textContent = value.toFixed(2); sendTargetPose(); }); } // Initialize app function init() { console.log('Initializing Reachy Mini Control Panel'); setupSliderHandlers(); connectWebSocket(); console.log('Control panel ready'); } // Start when DOM is loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }