Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Servo Web Controller</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| background: #1e1e2e; | |
| color: #dcdcdc; | |
| margin: 0; | |
| padding: 20px; | |
| text-align: center; | |
| } | |
| .container { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| background: #262626; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| h1 { | |
| color: #ffffff; | |
| margin-bottom: 20px; | |
| } | |
| .ip-section { | |
| margin: 20px 0; | |
| padding: 15px; | |
| background: #2d2d2d; | |
| border-radius: 5px; | |
| } | |
| input[type="text"] { | |
| padding: 10px; | |
| width: 200px; | |
| border: 1px solid #555; | |
| background: #1a1a1a; | |
| color: white; | |
| border-radius: 3px; | |
| } | |
| button { | |
| padding: 10px 20px; | |
| margin: 5px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| transition: background 0.3s; | |
| } | |
| .start-btn { | |
| background: #00ff64; | |
| color: black; | |
| } | |
| .start-btn:hover { | |
| background: #00cc50; | |
| } | |
| .stop-btn { | |
| background: #ff5050; | |
| color: white; | |
| } | |
| .stop-btn:hover { | |
| background: #cc4040; | |
| } | |
| .reset-btn { | |
| background: #5050ff; | |
| color: white; | |
| } | |
| .reset-btn:hover { | |
| background: #4040cc; | |
| } | |
| .status { | |
| margin: 20px 0; | |
| padding: 15px; | |
| border-radius: 5px; | |
| background: #2d2d2d; | |
| } | |
| .rotating { | |
| background: #00ff64; | |
| color: black; | |
| font-weight: bold; | |
| padding: 10px; | |
| border-radius: 5px; | |
| } | |
| .stopped { | |
| background: #ff5050; | |
| color: white; | |
| font-weight: bold; | |
| padding: 10px; | |
| border-radius: 5px; | |
| } | |
| .angles { | |
| font-size: 18px; | |
| margin: 15px 0; | |
| } | |
| .manual-control { | |
| margin: 20px 0; | |
| padding: 15px; | |
| background: #2d2d2d; | |
| border-radius: 5px; | |
| } | |
| .servo-control { | |
| margin: 10px 0; | |
| } | |
| input[type="range"] { | |
| width: 80%; | |
| margin: 0 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Servo Web Controller</h1> | |
| <div class="ip-section"> | |
| <h3>Set ESP32 IP Address</h3> | |
| <input type="text" id="ipInput" placeholder="192.168.1.100"> | |
| <button onclick="setIP()">Set IP</button> | |
| <div id="ipStatus"></div> | |
| </div> | |
| <div class="status"> | |
| <h3>Status</h3> | |
| <div id="rotationStatus" class="stopped">STOPPED</div> | |
| <div class="angles" id="anglesDisplay">Angles: 0°, 0°, 0°</div> | |
| <div id="currentIP">ESP32 IP: Not set</div> | |
| </div> | |
| <div class="controls"> | |
| <button class="start-btn" onclick="startRotation()">Hold A (Start Rotation)</button> | |
| <button class="stop-btn" onclick="stopRotation()">Release A (Emergency Stop)</button> | |
| <button class="reset-btn" onclick="resetServos()">Press R (Reset to 0°)</button> | |
| </div> | |
| <div class="manual-control"> | |
| <h3>Manual Servo Control</h3> | |
| <div class="servo-control"> | |
| Servo 1: <input type="range" id="servo1" min="0" max="180" value="0" onchange="setServoAngle(1, this.value)"> | |
| <span id="servo1Value">0°</span> | |
| </div> | |
| <div class="servo-control"> | |
| Servo 2: <input type="range" id="servo2" min="0" max="180" value="0" onchange="setServoAngle(2, this.value)"> | |
| <span id="servo2Value">0°</span> | |
| </div> | |
| <div class="servo-control"> | |
| Servo 3: <input type="range" id="servo3" min="0" max="180" value="0" onchange="setServoAngle(3, this.value)"> | |
| <span id="servo3Value">0°</span> | |
| </div> | |
| </div> | |
| <div id="message" style="margin-top: 20px; min-height: 40px;"></div> | |
| </div> | |
| <script> | |
| let currentIP = null; | |
| function updateStatus() { | |
| fetch('/get_status') | |
| .then(response => response.json()) | |
| .then(data => { | |
| document.getElementById('anglesDisplay').textContent = | |
| `Angles: ${data.current_angles[0]}°, ${data.current_angles[1]}°, ${data.current_angles[2]}°`; | |
| const statusElement = document.getElementById('rotationStatus'); | |
| if (data.rotation_active) { | |
| statusElement.textContent = 'ROTATING'; | |
| statusElement.className = 'rotating'; | |
| } else { | |
| statusElement.textContent = 'STOPPED'; | |
| statusElement.className = 'stopped'; | |
| } | |
| if (data.esp_ip) { | |
| currentIP = data.esp_ip; | |
| document.getElementById('currentIP').textContent = `ESP32 IP: ${data.esp_ip}`; | |
| } | |
| // Update sliders | |
| document.getElementById('servo1').value = data.current_angles[0]; | |
| document.getElementById('servo2').value = data.current_angles[1]; | |
| document.getElementById('servo3').value = data.current_angles[2]; | |
| document.getElementById('servo1Value').textContent = data.current_angles[0] + '°'; | |
| document.getElementById('servo2Value').textContent = data.current_angles[1] + '°'; | |
| document.getElementById('servo3Value').textContent = data.current_angles[2] + '°'; | |
| }) | |
| .catch(error => console.error('Error:', error)); | |
| } | |
| function setIP() { | |
| const ip = document.getElementById('ipInput').value; | |
| if (!ip) { | |
| showMessage('Please enter an IP address', 'error'); | |
| return; | |
| } | |
| fetch('/set_ip', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ip: ip}) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| showMessage(data.message, data.status); | |
| updateStatus(); | |
| }) | |
| .catch(error => { | |
| showMessage('Error setting IP: ' + error, 'error'); | |
| }); | |
| } | |
| function startRotation() { | |
| fetch('/start_rotation', { | |
| method: 'POST' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| showMessage(data.message, data.status); | |
| updateStatus(); | |
| }) | |
| .catch(error => { | |
| showMessage('Error starting rotation: ' + error, 'error'); | |
| }); | |
| } | |
| function stopRotation() { | |
| fetch('/stop_rotation', { | |
| method: 'POST' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| showMessage(data.message + ' - ' + data.stop_message, data.status); | |
| updateStatus(); | |
| }) | |
| .catch(error => { | |
| showMessage('Error stopping rotation: ' + error, 'error'); | |
| }); | |
| } | |
| function resetServos() { | |
| fetch('/reset_servos', { | |
| method: 'POST' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| showMessage(data.message, data.status); | |
| updateStatus(); | |
| }) | |
| .catch(error => { | |
| showMessage('Error resetting servos: ' + error, 'error'); | |
| }); | |
| } | |
| function setServoAngle(servoNum, angle) { | |
| document.getElementById(`servo${servoNum}Value`).textContent = angle + '°'; | |
| fetch('/set_angle', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({servo: servoNum, angle: parseInt(angle)}) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.status === 'success') { | |
| updateStatus(); | |
| } else { | |
| showMessage(data.message, 'error'); | |
| } | |
| }) | |
| .catch(error => { | |
| showMessage('Error setting angle: ' + error, 'error'); | |
| }); | |
| } | |
| function showMessage(message, type) { | |
| const messageElement = document.getElementById('message'); | |
| messageElement.textContent = message; | |
| messageElement.style.color = type === 'error' ? '#ff5050' : '#00ff64'; | |
| } | |
| // Update status every second | |
| setInterval(updateStatus, 1000); | |
| // Initial status update | |
| updateStatus(); | |
| </script> | |
| </body> | |
| </html> |