'use client'; import { useState, useEffect, useRef } from 'react'; export default function RobotDashboard() { const PI_IP = '172.17.172.18'; const streamUrl = `http://${PI_IP}:8080/stream`; const [controlMode, setControlMode] = useState('gamepad'); const [gamepadStatus, setGamepadStatus] = useState("Menunggu Gamepad..."); const [steering, setSteering] = useState({ x: 0, y: 0 }); const wsRef = useRef(null); const keysRef = useRef({ up: false, down: false, left: false, right: false }); const forceScanGamepad = () => { if (typeof navigator !== "undefined" && navigator.getGamepads) { const gamepads = navigator.getGamepads(); let activeGp = null; for (let i = 0; i < gamepads.length; i++) { if (gamepads[i] !== null) { activeGp = gamepads[i]; break; } } if (activeGp) setGamepadStatus(`🟢 TERHUBUNG: ${activeGp.id.substring(0, 15)}...`); } }; useEffect(() => { const ws = new WebSocket(`ws://${PI_IP}:8765`); ws.onopen = () => console.log("WebSocket Terhubung ke Pi!"); ws.onclose = () => console.log("WebSocket Terputus."); wsRef.current = ws; const handleKeyDown = (e: KeyboardEvent) => { if (['ArrowUp', 'w', 'W'].includes(e.key)) keysRef.current.up = true; if (['ArrowDown', 's', 'S'].includes(e.key)) keysRef.current.down = true; if (['ArrowLeft', 'a', 'A'].includes(e.key)) keysRef.current.left = true; if (['ArrowRight', 'd', 'D'].includes(e.key)) keysRef.current.right = true; }; const handleKeyUp = (e: KeyboardEvent) => { if (['ArrowUp', 'w', 'W'].includes(e.key)) keysRef.current.up = false; if (['ArrowDown', 's', 'S'].includes(e.key)) keysRef.current.down = false; if (['ArrowLeft', 'a', 'A'].includes(e.key)) keysRef.current.left = false; if (['ArrowRight', 'd', 'D'].includes(e.key)) keysRef.current.right = false; }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); const interval = setInterval(() => { let currentX = 0; let currentY = 0; if (controlMode === 'gamepad') { if (typeof navigator !== "undefined" && navigator.getGamepads) { const gamepads = navigator.getGamepads(); let activeGp = null; for (let i = 0; i < gamepads.length; i++) { if (gamepads[i] !== null) { activeGp = gamepads[i]; break; } } if (activeGp) { const rawX = activeGp.axes[0]; // SESUAI PERMINTAAN: RT(7) Maju, LT(6) Mundur const maju = activeGp.buttons[7].value; const mundur = activeGp.buttons[6].value; // PERBAIKAN 1: Karena Pi minta nilai minus untuk maju, pengurangannya dibalik (mundur - maju) const throttle = mundur - maju; currentX = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0; currentY = Number(throttle.toFixed(2)); } } } else if (controlMode === 'keyboard') { // PERBAIKAN 2: Keyboard disamakan. W/Up (maju) menghasilkan minus (-1), S/Down menghasilkan plus (+1) currentY = (keysRef.current.down ? 1 : 0) - (keysRef.current.up ? 1 : 0); currentX = (keysRef.current.right ? 1 : 0) - (keysRef.current.left ? 1 : 0); } setSteering({ x: currentX, y: currentY }); if (wsRef.current && wsRef.current.readyState === 1) { wsRef.current.send(JSON.stringify({ x: currentX, y: currentY })); } }, 100); return () => { clearInterval(interval); window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); if (wsRef.current) wsRef.current.close(); }; }, [controlMode, PI_IP]); // --- PERBAIKAN 3: VISUALIZER --- const maxRadius = 36; const dotX = steering.x * maxRadius; // Karena 'maju' nilainya sudah minus (-), dan di CSS nilai minus (-) membuat titik NAIK KE ATAS, // maka kita HAPUS perkalian (* -1) yang sebelumnya. Sekarang titiknya akan maju sempurna! const dotY = steering.y * maxRadius; return (

Robot Vision Control

Live Feed

{/* eslint-disable-next-line @next/next/no-img-element */} Robot view { e.currentTarget.style.display = 'none'; e.currentTarget.parentElement!.innerHTML = '
Kamera gagal dimuat.
Pastikan Pi dan HP berada dalam jaringan Tailscale/Wi-Fi yang sama.
'; }} />

Status Kontrol {controlMode === 'gamepad' && ( )}

{controlMode === 'keyboard' ? '⌨️ AKTIF: Gunakan W-A-S-D atau Panah' : gamepadStatus}
Visualizer (X / Y)
X: {steering.x} Y: {steering.y}
); }