From 82342fe592d3d2efdfb70533823f6e45c5989127 Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Tue, 11 Aug 2026 20:10:03 +0700 Subject: [PATCH] . --- app/page.tsx | 159 +++++++++++++++++++++++++++++---------------------- 1 file changed, 90 insertions(+), 69 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 159541e..3a54794 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,110 +3,123 @@ import { useState, useEffect, useRef } from 'react'; export default function RobotDashboard() { - // Ganti dengan IP Tailscale Pi atau IP lokal + // Sesuaikan dengan IP Raspberry Pi / Tailscale milikmu const PI_IP = '172.17.19.107'; const streamUrl = `http://${PI_IP}:8080/stream`; - const [gamepadStatus, setGamepadStatus] = useState("Menunggu..."); + // Tipe data disederhanakan agar tidak memicu error TS + const [controlMode, setControlMode] = useState('gamepad'); + const [gamepadStatus, setGamepadStatus] = useState("Menunggu Gamepad..."); const [steering, setSteering] = useState({ x: 0, y: 0 }); - // Referensi untuk WebSocket dengan penambahan tipe TypeScript - const wsRef = useRef(null); + // Menggunakan 'any' untuk menghindari error pengecekan tipe strict pada WebSocket + const wsRef = useRef(null); + + const keysRef = useRef({ up: false, down: false, left: false, right: false }); - // 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 })); - } - } + if (activeGp) setGamepadStatus(`🟢 TERHUBUNG: ${activeGp.id.substring(0, 15)}...`); } }; 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 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(() => { - 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; + 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]; + const lt = activeGp.buttons[6].value; + const rt = activeGp.buttons[7].value; + + const throttle = rt - lt; + currentX = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0; + currentY = Number(throttle.toFixed(2)); } } + } else if (controlMode === 'keyboard') { + currentY = (keysRef.current.up ? 1 : 0) - (keysRef.current.down ? 1 : 0); + currentX = (keysRef.current.right ? 1 : 0) - (keysRef.current.left ? 1 : 0); + } - 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: currentX, y: currentY }); - setSteering({ x, y }); - - // 3. KIRIM DATA KE RASPBERRY PI - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ x, y })); - } - } + // wsRef dipastikan aman dari TS error + 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]); return (
-
+

Robot Vision Control

- + +
+ + +
@@ -122,24 +135,32 @@ if (activeGp) {
-

- XBOX Controller +

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

- {gamepadStatus} + {controlMode === 'keyboard' ? '⌨️ AKTIF: Gunakan W-A-S-D atau Panah' : gamepadStatus}
-
Left Analog Stick
-
+
Visualizer (X / Y)
+
+
+
+