| | |
| | |
| |
|
| | const ROBOT_URL = 'localhost:8000'; |
| | const WS_URL = `ws://${ROBOT_URL}/api/move/ws/set_target`; |
| |
|
| | |
| | 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] |
| | } |
| | }; |
| |
|
| | |
| | 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') |
| | } |
| | }; |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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); |
| | } |
| | }; |
| | } |
| |
|
| | |
| | function updateConnectionStatus(connected) { |
| | if (connected) { |
| | elements.status.className = 'status connected'; |
| | elements.status.innerHTML = '<span><span class="status-dot green"></span>Connected to robot</span>'; |
| | } else { |
| | elements.status.className = 'status disconnected'; |
| | elements.status.innerHTML = '<span><span class="status-dot red"></span>Disconnected - Reconnecting...</span>'; |
| | } |
| | } |
| |
|
| | |
| | function enableControls(enabled) { |
| | Object.values(elements.sliders).forEach(slider => { |
| | slider.disabled = !enabled; |
| | }); |
| | } |
| |
|
| | |
| | function sendTargetPose() { |
| | if (!state.connected || !state.ws || state.ws.readyState !== WebSocket.OPEN) { |
| | console.warn('WebSocket not connected'); |
| | return; |
| | } |
| |
|
| | if (state.isRefreshing) { |
| | return; |
| | } |
| |
|
| | 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); |
| | } |
| | } |
| |
|
| | |
| | function setupSliderHandlers() { |
| | |
| | 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(); |
| | }); |
| |
|
| | |
| | elements.sliders.bodyYaw.addEventListener('input', (e) => { |
| | const value = parseFloat(e.target.value); |
| | state.currentPose.bodyYaw = value; |
| | elements.values.bodyYaw.textContent = value.toFixed(2); |
| | sendTargetPose(); |
| | }); |
| |
|
| | |
| | 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(); |
| | }); |
| | } |
| |
|
| | |
| | function init() { |
| | console.log('Initializing Reachy Mini Control Panel'); |
| |
|
| | setupSliderHandlers(); |
| | connectWebSocket(); |
| |
|
| | console.log('Control panel ready'); |
| | } |
| |
|
| | |
| | if (document.readyState === 'loading') { |
| | document.addEventListener('DOMContentLoaded', init); |
| | } else { |
| | init(); |
| | } |
| |
|