|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|