ROBOT SOCCER WEB SERVER VER 2
ROBOT SOCCER WEB SERVER VER 2
// Arduino IDE versi 2.2.1
// BOARD : LOLIN(WEMOS) D1 mini Lite (esp8266:esp8266:d1_mini_lite)
// ROBOT SOCCER NODEMCU - ANDROID LANDSCAPE MODE
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>
// connections for drive Motors
int PWM_A = D6;
int PWM_B = D7;
int DIR_A = D1;
int DIR_B = D2;
int DIR_C = D3;
int DIR_D = D4;
const int buzPin = D5;
const int ledPin = D8;
const int wifiLedPin = D0;
String command;
int SPEED = 1023;
ESP8266WebServer server(80);
unsigned long previousMillis = 0;
const char* sta_ssid = "www.asun86.com";
const char* sta_password = "";
// HTML Page untuk Android Landscape
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Robot Soccer - Android Mode</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: 'Roboto', Arial, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
touch-action: manipulation;
}
.container {
display: flex;
height: 100vh;
padding: 8px;
gap: 8px;
}
/* Panel Kiri - Kontrol Maju Mundur */
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 15px;
padding: 0 5px;
}
/* Panel Kanan - Kontrol Belok dan Putar */
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 20px;
padding: 0 5px;
}
/* Panel Tengah - Info dan Kontrol Tambahan */
.center-panel {
flex: 1.2;
display: flex;
flex-direction: column;
justify-content: space-between;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 12px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
}
/* Grup Kontrol Kanan - Belok */
.turn-group {
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 12px;
border: 1px solid rgba(255,255,255,0.2);
}
/* Grup Kontrol Kanan - Putar */
.rotate-group {
background: rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 12px;
border: 1px solid rgba(255,255,255,0.2);
}
.group-title {
font-size: 12px;
color: white;
text-align: center;
margin-bottom: 10px;
opacity: 0.9;
font-weight: bold;
text-transform: uppercase;
}
.button-row {
display: flex;
gap: 10px;
justify-content: space-between;
}
.brand-section {
text-align: center;
color: white;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(255,255,255,0.2);
}
.rfi-title {
font-size: 24px;
font-weight: bold;
color: gold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
line-height: 1.1;
}
.rfi-subtitle {
font-size: 11px;
color: white;
margin-top: 2px;
font-weight: bold;
}
.website-link {
font-size: 11px;
color: #e0e0e0;
margin-top: 3px;
font-weight: bold;
}
.robot-title {
font-size: 16px;
font-weight: bold;
margin-top: 8px;
color: white;
}
.control-btn {
border: none;
border-radius: 15px;
color: white;
font-size: 12px;
font-weight: bold;
cursor: pointer;
transition: all 0.1s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
padding: 12px 8px;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
min-height: 80px;
flex: 1;
}
.control-btn:active {
transform: scale(0.92);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
filter: brightness(1.2);
}
/* Tombol Maju dan Mundur */
.btn-forward, .btn-backward {
background: linear-gradient(145deg, #27ae60, #2ecc71);
font-size: 14px;
min-height: 100px;
}
/* Tombol Belok */
.btn-turn-left, .btn-turn-right {
background: linear-gradient(145deg, #e67e22, #f39c12);
font-size: 12px;
}
/* Tombol Putar */
.btn-rotate-left, .btn-rotate-right {
background: linear-gradient(145deg, #9b59b6, #8e44ad);
font-size: 12px;
}
.btn-horn {
background: linear-gradient(145deg, #e74c3c, #c0392b);
font-size: 14px !important;
padding: 12px 8px;
}
.btn-light {
background: linear-gradient(145deg, #f1c40f, #f39c12);
color: #2c3e50;
font-size: 14px !important;
padding: 12px 8px;
}
.info-section {
text-align: center;
color: white;
}
.keyboard-help {
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
padding: 8px;
margin-bottom: 5px;
font-size: 10px;
border: 1px solid rgba(255,255,255,0.1);
}
.keyboard-row {
display: flex;
justify-content: space-between;
margin: 3px 0;
}
.key-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.key {
background: rgba(255,255,255,0.9);
color: #2c3e50;
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
font-size: 9px;
margin-bottom: 2px;
}
.key-desc {
font-size: 8px;
color: #e0e0e0;
}
.info-title {
font-size: 10px;
opacity: 0.8;
margin-bottom: 3px;
color: #333;
font-weight: bold;
}
.info-value {
font-size: 14px;
font-weight: bold;
color: #000;
}
.speed-control {
background: rgba(255, 255, 255, 0.95);
padding: 10px;
border-radius: 10px;
margin: 8px 0;
border: 2px solid #333;
}
.speed-slider {
width: 100%;
height: 8px;
border-radius: 4px;
background: #ddd;
outline: none;
-webkit-appearance: none;
}
.speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #3498db;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
transition: all 0.1s ease;
}
.speed-slider::-webkit-slider-thumb:active {
transform: scale(1.1);
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #2ecc71;
margin-right: 5px;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 8px 0;
}
.action-btn {
padding: 12px 8px;
border: none;
border-radius: 10px;
background: linear-gradient(145deg, #34495e, #2c3e50);
color: white;
font-size: 12px;
font-weight: bold;
cursor: pointer;
transition: all 0.1s ease;
}
.action-btn:active {
transform: scale(0.95);
}
.connection-status {
background: rgba(255, 255, 255, 0.95);
padding: 8px;
border-radius: 8px;
margin-top: 8px;
font-size: 10px;
text-align: center;
border: 2px solid #333;
color: #000;
font-weight: bold;
}
.connection-status div {
color: #000;
font-weight: bold;
}
/* Responsive untuk landscape orientation */
@media (max-height: 500px) {
.btn-forward, .btn-backward {
min-height: 80px;
font-size: 12px;
}
.control-btn {
min-height: 70px;
font-size: 11px;
padding: 10px 6px;
}
.center-panel {
padding: 8px;
}
.rfi-title {
font-size: 20px;
}
}
/* Efek responsive instant */
.instant-feedback {
transition: all 0.05s ease !important;
}
.black-bold {
color: #000 !important;
font-weight: bold !important;
}
/* Tombol aktif state */
.active-btn {
filter: brightness(1.3) !important;
transform: scale(0.95) !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.4) !important;
}
</style>
</head>
<body>
<div class="container">
<!-- Panel Kiri - Kontrol Maju Mundur -->
<div class="left-panel">
<button class="control-btn btn-forward instant-feedback"
ontouchstart="sendCommand('F')"
ontouchend="sendCommand('S')">
<div>MAJU</div>
</button>
<button class="control-btn btn-backward instant-feedback"
ontouchstart="sendCommand('B')"
ontouchend="sendCommand('S')">
<div>MUNDUR</div>
</button>
</div>
<!-- Panel Tengah - Info dan Kontrol Tambahan -->
<div class="center-panel">
<div class="info-section">
<div class="brand-section">
<div class="rfi-title">RFI</div>
<div class="rfi-subtitle">Robotic Future Indonesia</div>
<div class="website-link">www.asun86.my.id || www.asun86.com</div>
<div class="robot-title">ROBOT SOCCER</div>
</div>
<div class="keyboard-help">
<div class="keyboard-row">
<div class="key-item">
<div class="key">W / ↑</div>
<div class="key-desc">MAJU</div>
</div>
<div class="key-item">
<div class="key">S / ↓</div>
<div class="key-desc">MUNDUR</div>
</div>
</div>
<div class="keyboard-row">
<div class="key-item">
<div class="key">A / ←</div>
<div class="key-desc">BELOK KIRI</div>
</div>
<div class="key-item">
<div class="key">D / →</div>
<div class="key-desc">BELOK KANAN</div>
</div>
</div>
<div class="keyboard-row">
<div class="key-item">
<div class="key">Q</div>
<div class="key-desc">PUTAR KIRI</div>
</div>
<div class="key-item">
<div class="key">E</div>
<div class="key-desc">PUTAR KANAN</div>
</div>
</div>
</div>
<div class="speed-control">
<div class="info-title">KECEPATAN</div>
<div class="info-value" id="speed-value">1023</div>
<input type="range" class="speed-slider instant-feedback" id="speed" min="330" max="1023" value="1023" step="10">
</div>
<div class="connection-status">
<div>IP: <span id="ip-address" class="black-bold">192.168.11.86</span></div>
<div>Status: <span id="connection-status" class="black-bold">SIAP</span></div>
</div>
</div>
<div class="action-buttons">
<button class="action-btn btn-horn instant-feedback" ontouchstart="sendCommand('V')">
<div>HORN</div>
</button>
<button class="action-btn btn-light instant-feedback" onclick="toggleLight()" id="btn-light">
<div>LAMPU</div>
</button>
</div>
</div>
<!-- Panel Kanan - Kontrol Belok dan Putar -->
<div class="right-panel">
<!-- Grup Belok -->
<div class="turn-group">
<div class="group-title">KONTROL BELOK</div>
<div class="button-row">
<button class="control-btn btn-turn-left instant-feedback"
ontouchstart="sendCommand('L')"
ontouchend="sendCommand('S')">
<div>BELOK<br>KIRI</div>
</button>
<button class="control-btn btn-turn-right instant-feedback"
ontouchstart="sendCommand('R')"
ontouchend="sendCommand('S')">
<div>BELOK<br>KANAN</div>
</button>
</div>
</div>
<!-- Grup Putar -->
<div class="rotate-group">
<div class="group-title">KONTROL PUTAR</div>
<div class="button-row">
<button class="control-btn btn-rotate-left instant-feedback"
ontouchstart="sendCommand('Q')"
ontouchend="sendCommand('S')">
<div>PUTAR<br>KIRI</div>
</button>
<button class="control-btn btn-rotate-right instant-feedback"
ontouchstart="sendCommand('E')"
ontouchend="sendCommand('S')">
<div>PUTAR<br>KANAN</div>
</button>
</div>
</div>
</div>
</div>
<script>
let lightOn = false;
let activeCommand = '';
let lastCommandTime = 0;
const COMMAND_DELAY = 50; // ms delay antara command
// Update IP address
document.getElementById('ip-address').textContent = window.location.hostname || '192.168.11.86';
function sendCommand(cmd) {
const now = Date.now();
if (cmd === activeCommand && (now - lastCommandTime) < COMMAND_DELAY) {
return;
}
// Remove active class from all buttons
document.querySelectorAll('.control-btn').forEach(btn => {
btn.classList.remove('active-btn');
});
// Add active class to pressed button
if (['F', 'B', 'L', 'R', 'Q', 'E'].includes(cmd)) {
const activeBtn = event.target.closest('.control-btn');
if (activeBtn) {
activeBtn.classList.add('active-btn');
}
}
fetch('/?State=' + cmd)
.then(response => response.text())
.then(data => {
console.log('Command sent:', cmd);
activeCommand = cmd;
lastCommandTime = now;
updateStatus('Aktif: ' + getCommandName(cmd));
})
.catch(error => {
console.error('Error:', error);
updateStatus('Error!');
});
}
function getCommandName(cmd) {
const commands = {
'F': 'MAJU', 'B': 'MUNDUR', 'L': 'BELOK KIRI', 'R': 'BELOK KANAN',
'Q': 'PUTAR KIRI', 'E': 'PUTAR KANAN', 'S': 'STOP', 'V': 'HORN',
'W': 'LAMPU ON', 'w': 'LAMPU OFF'
};
return commands[cmd] || cmd;
}
function toggleLight() {
let cmd = lightOn ? 'w' : 'W';
sendCommand(cmd);
lightOn = !lightOn;
const btnLight = document.getElementById('btn-light');
btnLight.innerHTML = lightOn ? '<div>LAMPU ON</div>' : '<div>LAMPU OFF</div>';
btnLight.style.background = lightOn ?
'linear-gradient(145deg, #f39c12, #e67e22)' :
'linear-gradient(145deg, #f1c40f, #f39c12)';
}
function updateStatus(message) {
const statusElement = document.getElementById('connection-status');
statusElement.textContent = message;
statusElement.style.color = '#27ae60';
statusElement.style.fontWeight = 'bold';
setTimeout(() => {
statusElement.textContent = 'SIAP';
statusElement.style.color = '#000';
statusElement.style.fontWeight = 'bold';
}, 1500);
}
// Speed control
document.getElementById('speed').addEventListener('input', function() {
let speed = this.value;
document.getElementById('speed-value').textContent = speed;
sendCommand(speed);
});
// Keyboard control untuk testing di desktop
document.addEventListener('keydown', function(event) {
event.preventDefault();
const key = event.key.toLowerCase();
switch(key) {
case 'arrowup':
case 'w':
sendCommand('F');
break;
case 'arrowdown':
case 's':
sendCommand('B');
break;
case 'arrowleft':
case 'a':
sendCommand('L');
break;
case 'arrowright':
case 'd':
sendCommand('R');
break;
case 'q':
sendCommand('Q');
break;
case 'e':
sendCommand('E');
break;
case ' ':
sendCommand('S');
break;
case 'h':
sendCommand('V');
break;
case 'l':
toggleLight();
break;
}
});
document.addEventListener('keyup', function(event) {
const key = event.key.toLowerCase();
if (['arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'w', 'a', 's', 'd', 'q', 'e'].includes(key)) {
sendCommand('S');
}
});
// Prevent context menu on long press
document.addEventListener('contextmenu', function(event) {
event.preventDefault();
return false;
});
// Improved touch handling
document.addEventListener('touchstart', function(event) {
if (event.target.classList.contains('control-btn') || event.target.classList.contains('action-btn')) {
event.preventDefault();
}
}, { passive: false });
// Lock orientation (hint untuk browser)
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock('landscape').catch(function(error) {
console.log('Orientation lock failed: ', error);
});
}
// Initial focus
window.focus();
// Force stop on page unload
window.addEventListener('beforeunload', function() {
fetch('/?State=S').catch(() => {});
});
</script>
</body>
</html>
)rawliteral";
// Deklarasi fungsi
void HTTP_handleRoot(void);
void Forward();
void Backward();
void TurnRight();
void TurnLeft();
void RotateRight();
void RotateLeft();
void Stop();
void BeepHorn();
void TurnLightOn();
void TurnLightOff();
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("🤖 Robot Soccer - Android Landscape Mode");
Serial.println("--------------------------------------");
pinMode(buzPin, OUTPUT);
pinMode(ledPin, OUTPUT);
pinMode(wifiLedPin, OUTPUT);
digitalWrite(buzPin, LOW);
digitalWrite(ledPin, LOW);
digitalWrite(wifiLedPin, HIGH);
// Set all the motor control pins to outputs
pinMode(PWM_A, OUTPUT);
pinMode(PWM_B, OUTPUT);
pinMode(DIR_A, OUTPUT);
pinMode(DIR_B, OUTPUT);
pinMode(DIR_C, OUTPUT);
pinMode(DIR_D, OUTPUT);
// Turn off motors - Initial state
digitalWrite(DIR_A, LOW);
digitalWrite(DIR_B, LOW);
digitalWrite(DIR_C, LOW);
digitalWrite(DIR_D, LOW);
analogWrite(PWM_A, 0);
analogWrite(PWM_B, 0);
// WiFi connection
WiFi.mode(WIFI_STA);
WiFi.begin(sta_ssid, sta_password);
Serial.println("");
Serial.print("Connecting to: ");
Serial.println(sta_ssid);
unsigned long currentMillis = millis();
previousMillis = currentMillis;
while (WiFi.status() != WL_CONNECTED && currentMillis - previousMillis <= 10000) {
delay(500);
Serial.print(".");
currentMillis = millis();
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("✅ WiFi Connected");
Serial.print("📱 IP: ");
Serial.println(WiFi.localIP());
digitalWrite(wifiLedPin, LOW);
} else {
WiFi.mode(WIFI_AP);
String hostname = "soccer-bot-" + String(ESP.getChipId(), HEX);
WiFi.softAP(hostname.c_str());
IPAddress myIP = WiFi.softAPIP();
Serial.println("");
Serial.println("📱 AP Mode Activated");
Serial.print("📍 AP IP: ");
Serial.println(myIP);
digitalWrite(wifiLedPin, HIGH);
}
server.on("/", HTTP_handleRoot);
server.onNotFound(HTTP_handleRoot);
server.begin();
ArduinoOTA.begin();
Serial.println("🚀 Server started - Ready for Android control");
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
command = server.arg("State");
if (command == "F") {
Forward();
Serial.println("📱 MAJU");
analogWrite(PWM_A, SPEED);
analogWrite(PWM_B, SPEED);
}
else if (command == "B") {
Backward();
Serial.println("📱 MUNDUR");
analogWrite(PWM_A, SPEED);
analogWrite(PWM_B, SPEED);
}
else if (command == "R") {
TurnRight();
Serial.println("📱 BELOK KANAN");
analogWrite(PWM_A, SPEED);
analogWrite(PWM_B, SPEED);
}
else if (command == "L") {
TurnLeft();
Serial.println("📱 BELOK KIRI");
analogWrite(PWM_A, SPEED);
analogWrite(PWM_B, SPEED);
}
else if (command == "Q") {
RotateLeft();
Serial.println("📱 PUTAR KIRI");
analogWrite(PWM_A, SPEED);
analogWrite(PWM_B, SPEED);
}
else if (command == "E") {
RotateRight();
Serial.println("📱 PUTAR KANAN");
analogWrite(PWM_A, SPEED);
analogWrite(PWM_B, SPEED);
}
else if (command == "S") {
Stop();
Serial.println("📱 STOP");
analogWrite(PWM_A, 0);
analogWrite(PWM_B, 0);
}
else if (command == "V") {
BeepHorn();
Serial.println("📱 HORN");
}
else if (command == "W") {
TurnLightOn();
Serial.println("📱 LAMPU ON");
}
else if (command == "w") {
TurnLightOff();
Serial.println("📱 LAMPU OFF");
}
else {
int speedValue = command.toInt();
if (speedValue >= 330 && speedValue <= 1023) {
SPEED = speedValue;
Serial.println("🎚️ Speed: " + String(SPEED));
}
}
delay(10); // Small delay for stability
}
void HTTP_handleRoot(void) {
server.send(200, "text/html", htmlPage);
}
// Movement functions
void Forward() {
digitalWrite(DIR_B, HIGH);
digitalWrite(DIR_A, LOW);
digitalWrite(DIR_D, HIGH);
digitalWrite(DIR_C, LOW);
}
void Backward() {
digitalWrite(DIR_B, LOW);
digitalWrite(DIR_A, HIGH);
digitalWrite(DIR_D, LOW);
digitalWrite(DIR_C, HIGH);
}
void TurnRight() {
// Belok kanan - motor kiri maju, motor kanan diam
digitalWrite(DIR_A, LOW);
digitalWrite(DIR_B, HIGH);
digitalWrite(DIR_C, LOW);
digitalWrite(DIR_D, LOW);
}
void TurnLeft() {
// Belok kiri - motor kiri diam, motor kanan maju
digitalWrite(DIR_A, LOW);
digitalWrite(DIR_B, LOW);
digitalWrite(DIR_C, LOW);
digitalWrite(DIR_D, HIGH);
}
void RotateRight() {
// Putar kanan - motor kiri maju, motor kanan mundur
digitalWrite(DIR_B, HIGH);
digitalWrite(DIR_A, LOW);
digitalWrite(DIR_D, LOW);
digitalWrite(DIR_C, HIGH);
}
void RotateLeft() {
// Putar kiri - motor kiri mundur, motor kanan maju
digitalWrite(DIR_B, LOW);
digitalWrite(DIR_A, HIGH);
digitalWrite(DIR_D, HIGH);
digitalWrite(DIR_C, LOW);
}
void Stop() {
digitalWrite(DIR_A, LOW);
digitalWrite(DIR_B, LOW);
digitalWrite(DIR_C, LOW);
digitalWrite(DIR_D, LOW);
}
void BeepHorn() {
digitalWrite(buzPin, HIGH);
delay(100); // Reduced delay for faster response
digitalWrite(buzPin, LOW);
}
void TurnLightOn() {
digitalWrite(ledPin, HIGH);
}
void TurnLightOff() {
digitalWrite(ledPin, LOW);
}
cpp
const char* sta_ssid = "www.asun86.com";
const char* sta_password = "";
Mode STA (Station): Menghubungkan ke WiFi "www.asun86.com"
Mode AP (Access Point): Jika gagal konek, membuat hotspot sendiri dengan nama "soccer-bot-[ID]"
IP Address: Bisa dilihat di Serial Monitor setelah koneksi berhasil
Pin Motor:
PWM_A = D6, PWM_B = D7 - Kontrol kecepatan
DIR_A = D1, DIR_B = D2, DIR_C = D3, DIR_D = D4 - Kontrol arah
Fungsi Gerakan:
Forward() - Maju
Backward() - Mundur
TurnRight() - Belok kanan
TurnLeft() - Belok kiri
RotateRight() - Putar kanan (rotasi)
RotateLeft() - Putar kiri (rotasi)
Stop() - Berhenti
Buzzer (D5): Horn/klakson
LED (D8): Lampu robot
WiFi LED (D0): Indikator status WiFi
text
1. Nyalakan robot
2. Buka WiFi di smartphone/PC
3. Cari SSID: "www.asun86.com"
4. Konek tanpa password
5. Buka browser, ketik: 192.168.11.86
text
1. Jika tidak ada WiFi "www.asun86.com"
2. Robot otomatis buat hotspot: "soccer-bot-[ID]"
3. Konek ke hotspot tersebut
4. Buka browser, ketik: 192.168.4.1
text
W / ↑ = MAJU (Forward)
S / ↓ = MUNDUR (Backward)
A / ← = BELOK KIRI (Turn Left)
D / → = BELOK KANAN (Turn Right)
text
Q = PUTAR KIRI (Rotate Left)
E = PUTAR KANAN (Rotate Right)
text
H = HORN (Klakson)
L = LAMPU (Toggle On/Off)
[SPACE] = STOP (Emergency stop)
Slider di web: Atur kecepatan 330-1023
Nilai 1023: Kecepatan maksimum
Nilai 330: Kecepatan minimum
Tombol langsung merespons sentuhan
Motor berhenti cepat saat tombol dilepas
Animasi visual saat tombol ditekan
Belok: Satu motor aktif, satu diam
Putar: Motor berputar berlawanan arah
Stop: Semua motor langsung mati
Optimized untuk Android landscape
Touch-friendly dengan tombol besar
Real-time status koneksi
Keyboard support untuk testing
Auto-stop saat tombol dilepas
Emergency stop dengan spasi
Delay minimal antar perintah
Konek ke WiFi robot
Buka IP address di browser
Gunakan tombol di web atau keyboard
Atur kecepatan sesuai kebutuhan
Tekan tombol untuk gerakan, lepas untuk stop
Motor tidak jalan: Cek koneksi baterai dan kabel motor
Tidak bisa konek: Restart robot, cek LED indikator
Web tidak loading: Refresh browser, clear cache
Gerakan tidak presisi: Kalibrasi kecepatan di slider
Koding ini memberikan kontrol penuh atas robot soccer dengan interface yang user-friendly dan responsif! 🚀