'use client'; import { useState, useEffect, useRef } from 'react'; export default function RobotDashboard() { // Ganti dengan IP Tailscale Pi atau IP lokal const PI_IP = '172.17.19.107'; const streamUrl = `http://${PI_IP}:8080/stream`; const [gamepadStatus, setGamepadStatus] = useState("Menunggu..."); const [steering, setSteering] = useState({ x: 0, y: 0 }); // Referensi untuk WebSocket dengan penambahan tipe TypeScript const wsRef = useRef(null); // FUNGSI INI KEMBALI DITAMBAHKAN AGAR TOMBOL BISA BEKERJA 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)}...`); // Joystick Kiri (hanya sumbu X untuk belok) const rawX = activeGp.axes[0]; // Membaca Trigger LT dan RT (nilainya 0.0 hingga 1.0) const lt = activeGp.buttons[6].value; const rt = activeGp.buttons[7].value; // Logika Gas: RT positif (Maju), LT negatif (Mundur) const throttle = rt - lt; const x = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0; const y = Number(throttle.toFixed(2)); // Kita tetap namakan 'y' agar backend mudah membacanya setSteering({ x, y }); // 3. KIRIM DATA KE RASPBERRY PI if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ x, y })); } } } }; useEffect(() => { // 1. Buka Koneksi WebSocket ke Pi 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; // 2. Loop Joystick const interval = setInterval(() => { 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)}...`); const rawX = activeGp.axes[0]; const rawY = activeGp.axes[1]; const x = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0; const y = Math.abs(rawY) > 0.15 ? Number(rawY.toFixed(2)) : 0; setSteering({ x, y }); // 3. KIRIM DATA KE RASPBERRY PI if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ x, y })); } } } }, 100); return () => { clearInterval(interval); if (wsRef.current) wsRef.current.close(); }; }, []); return (

Robot Vision Control

Live Feed

{/* eslint-disable-next-line @next/next/no-img-element */} Robot view

XBOX Controller

{gamepadStatus}
Left Analog Stick
X: {steering.x} Y: {steering.y}
); }