|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Robot Controller</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
body { |
|
min-height: 100vh; |
|
background-color: #111827; |
|
color: white; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
padding: 1.5rem; |
|
} |
|
.container { |
|
max-width: 28rem; |
|
width: 100%; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 2rem; |
|
} |
|
.header { |
|
text-align: center; |
|
} |
|
.title { |
|
font-size: 2rem; |
|
font-weight: bold; |
|
margin-bottom: 0.5rem; |
|
} |
|
.status { |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
padding: 0.5rem 1rem; |
|
border-radius: 9999px; |
|
background-color: rgba(239, 68, 68, 0.2); |
|
color: rgb(248, 113, 113); |
|
font-size: 0.875rem; |
|
transition: all 0.3s ease; |
|
} |
|
.status.connected { |
|
background-color: rgba(34, 197, 94, 0.2); |
|
color: rgb(74, 222, 128); |
|
} |
|
.status::before { |
|
content: ''; |
|
width: 0.5rem; |
|
height: 0.5rem; |
|
background-color: currentColor; |
|
border-radius: 50%; |
|
animation: pulse 2s infinite; |
|
} |
|
@keyframes pulse { |
|
0% { opacity: 1; } |
|
50% { opacity: 0.4; } |
|
100% { opacity: 1; } |
|
} |
|
.manual-status { |
|
margin-top: 1rem; |
|
font-size: 1rem; |
|
font-weight: bold; |
|
text-align: center; |
|
padding: 0.5rem; |
|
background-color: rgba(34, 197, 94, 0.2); |
|
color: rgb(74, 222, 128); |
|
border-radius: 9999px; |
|
transition: all 0.3s ease; |
|
} |
|
.manual-status.deactivated { |
|
background-color: rgba(239, 68, 68, 0.2); |
|
color: rgb(248, 113, 113); |
|
} |
|
.control-pad { |
|
display: grid; |
|
grid-template-columns: repeat(3, 1fr); |
|
gap: 1rem; |
|
max-width: 240px; |
|
margin: 0 auto; |
|
place-items: center; |
|
} |
|
.control-button { |
|
width: 4rem; |
|
height: 4rem; |
|
border-radius: 50%; |
|
border: none; |
|
background-color: #4f46e5; |
|
color: white; |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
transition: all 0.2s; |
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
} |
|
.control-button:hover { |
|
background-color: #4338ca; |
|
transform: translateY(-2px); |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|
} |
|
.control-button:active { |
|
background-color: #3730a3; |
|
transform: translateY(0); |
|
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); |
|
} |
|
.emergency-stop { |
|
grid-column: 2; |
|
grid-row: 2; |
|
background-color: #dc2626 !important; |
|
width: 4.5rem !important; |
|
height: 4.5rem !important; |
|
border: 3px solid #ef4444; |
|
} |
|
.emergency-stop:hover { |
|
background-color: #b91c1c !important; |
|
} |
|
.emergency-stop:active { |
|
background-color: #991b1b !important; |
|
} |
|
.control-button svg { |
|
width: 1.5rem; |
|
height: 1.5rem; |
|
fill: none; |
|
stroke: currentColor; |
|
stroke-width: 2; |
|
stroke-linecap: round; |
|
stroke-linejoin: round; |
|
} |
|
.command-log { |
|
background-color: #1f2937; |
|
border-radius: 0.5rem; |
|
padding: 1rem; |
|
} |
|
.command-log h2 { |
|
font-size: 1.125rem; |
|
font-weight: 600; |
|
margin-bottom: 0.5rem; |
|
} |
|
.log-container { |
|
height: 12rem; |
|
overflow-y: auto; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.25rem; |
|
} |
|
.log-entry { |
|
padding: 0.25rem 0.5rem; |
|
background-color: rgba(55, 65, 81, 0.5); |
|
border-radius: 0.25rem; |
|
font-size: 0.875rem; |
|
} |
|
|
|
.btn-up { grid-column: 2; grid-row: 1; } |
|
.btn-left { grid-column: 1; grid-row: 2; } |
|
.btn-right { grid-column: 3; grid-row: 2; } |
|
.btn-down { grid-column: 2; grid-row: 3; } |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1 class="title">Robot Controller</h1> |
|
<div class="status">Disconnected</div> |
|
<div id="manualModeStatus" class="manual-status deactivated">Manual Mode Deactivated</div> |
|
</div> |
|
|
|
<div class="control-pad"> |
|
<button class="control-button btn-up" data-direction="forward"> |
|
<svg viewBox="0 0 24 24"> |
|
<path d="M12 19V5M5 12l7-7 7 7"/> |
|
</svg> |
|
</button> |
|
<button class="control-button btn-left" data-direction="left"> |
|
<svg viewBox="0 0 24 24"> |
|
<path d="M19 12H5m7-7-7 7 7 7"/> |
|
</svg> |
|
</button> |
|
<button class="control-button emergency-stop" data-direction="emergency_stop"> |
|
<svg viewBox="0 0 24 24"> |
|
<rect x="6" y="6" width="12" height="12" fill="white"/> |
|
</svg> |
|
</button> |
|
<button class="control-button btn-right" data-direction="right"> |
|
<svg viewBox="0 0 24 24"> |
|
<path d="M5 12h14m-7-7 7 7-7 7"/> |
|
</svg> |
|
</button> |
|
<button class="control-button btn-down" data-direction="backward"> |
|
<svg viewBox="0 0 24 24"> |
|
<path d="M12 5v14m7-7-7 7-7-7"/> |
|
</svg> |
|
</button> |
|
</div> |
|
|
|
<div class="command-log"> |
|
<h2>Command Log</h2> |
|
<div class="log-container" id="logContainer"></div> |
|
</div> |
|
</div> |
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> |
|
<script> |
|
const socket = io(); |
|
const buttons = document.querySelectorAll('.control-button'); |
|
const status = document.querySelector('.status'); |
|
const logContainer = document.getElementById('logContainer'); |
|
const manualModeStatus = document.getElementById('manualModeStatus'); |
|
let commandInterval = null; |
|
let activeCommand = null; |
|
let isButtonPressed = false; |
|
|
|
socket.on('connect', () => { |
|
status.textContent = 'Connected'; |
|
status.classList.add('connected'); |
|
}); |
|
|
|
socket.on('disconnect', () => { |
|
status.textContent = 'Disconnected'; |
|
status.classList.remove('connected'); |
|
}); |
|
|
|
|
|
socket.on('manual_mode_status', (data) => { |
|
if (data.status === 'activated') { |
|
manualModeStatus.textContent = 'Manual Mode Activated'; |
|
manualModeStatus.classList.remove('deactivated'); |
|
} else { |
|
manualModeStatus.textContent = 'Manual Mode Deactivated'; |
|
manualModeStatus.classList.add('deactivated'); |
|
} |
|
}); |
|
|
|
function logCommand(command) { |
|
const entry = document.createElement('div'); |
|
entry.className = 'log-entry'; |
|
entry.textContent = command; |
|
logContainer.insertBefore(entry, logContainer.firstChild); |
|
if (logContainer.children.length > 50) { |
|
logContainer.removeChild(logContainer.lastChild); |
|
} |
|
} |
|
|
|
function sendCommand(command) { |
|
socket.emit('control_command', command); |
|
logCommand(command); |
|
} |
|
|
|
function startCommand(command) { |
|
if (command === 'emergency_stop') { |
|
stopCommand(); |
|
sendCommand('emergency_stop'); |
|
return; |
|
} |
|
if (commandInterval) { |
|
clearInterval(commandInterval); |
|
} |
|
activeCommand = command; |
|
isButtonPressed = true; |
|
sendCommand(command); |
|
commandInterval = setInterval(() => { |
|
if (isButtonPressed) { |
|
sendCommand(command); |
|
} |
|
}, 100); |
|
} |
|
|
|
function stopCommand() { |
|
isButtonPressed = false; |
|
if (commandInterval) { |
|
clearInterval(commandInterval); |
|
commandInterval = null; |
|
} |
|
if (activeCommand && activeCommand !== 'emergency_stop') { |
|
sendCommand('stop'); |
|
} |
|
activeCommand = null; |
|
} |
|
|
|
buttons.forEach(button => { |
|
const direction = button.dataset.direction; |
|
|
|
button.addEventListener('mousedown', () => { |
|
startCommand(direction); |
|
if (direction !== 'emergency_stop') { |
|
button.style.backgroundColor = '#3730a3'; |
|
} |
|
}); |
|
|
|
button.addEventListener('mouseup', () => { |
|
if (direction !== 'emergency_stop') { |
|
stopCommand(); |
|
button.style.backgroundColor = ''; |
|
} |
|
}); |
|
|
|
button.addEventListener('mouseleave', () => { |
|
if (isButtonPressed && direction !== 'emergency_stop') { |
|
stopCommand(); |
|
button.style.backgroundColor = ''; |
|
} |
|
}); |
|
|
|
button.addEventListener('touchstart', (e) => { |
|
e.preventDefault(); |
|
startCommand(direction); |
|
if (direction !== 'emergency_stop') { |
|
button.style.backgroundColor = '#3730a3'; |
|
} |
|
}); |
|
|
|
button.addEventListener('touchend', (e) => { |
|
e.preventDefault(); |
|
if (direction !== 'emergency_stop') { |
|
stopCommand(); |
|
button.style.backgroundColor = ''; |
|
} |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|