.
This commit is contained in:
parent
aee55b8d5d
commit
82342fe592
131
app/page.tsx
131
app/page.tsx
@ -3,65 +3,62 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export default function RobotDashboard() {
|
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 PI_IP = '172.17.19.107';
|
||||||
const streamUrl = `http://${PI_IP}:8080/stream`;
|
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 });
|
const [steering, setSteering] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
// Referensi untuk WebSocket dengan penambahan tipe TypeScript
|
// Menggunakan 'any' untuk menghindari error pengecekan tipe strict pada WebSocket
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const keysRef = useRef({ up: false, down: false, left: false, right: false });
|
||||||
|
|
||||||
// FUNGSI INI KEMBALI DITAMBAHKAN AGAR TOMBOL BISA BEKERJA
|
|
||||||
const forceScanGamepad = () => {
|
const forceScanGamepad = () => {
|
||||||
if (typeof navigator !== "undefined" && navigator.getGamepads) {
|
if (typeof navigator !== "undefined" && navigator.getGamepads) {
|
||||||
const gamepads = navigator.getGamepads();
|
const gamepads = navigator.getGamepads();
|
||||||
let activeGp = null;
|
let activeGp = null;
|
||||||
|
|
||||||
for (let i = 0; i < gamepads.length; i++) {
|
for (let i = 0; i < gamepads.length; i++) {
|
||||||
if (gamepads[i] !== null) {
|
if (gamepads[i] !== null) {
|
||||||
activeGp = gamepads[i];
|
activeGp = gamepads[i];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (activeGp) setGamepadStatus(`🟢 TERHUBUNG: ${activeGp.id.substring(0, 15)}...`);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
// 1. Buka Koneksi WebSocket ke Pi
|
|
||||||
const ws = new WebSocket(`ws://${PI_IP}:8765`);
|
const ws = new WebSocket(`ws://${PI_IP}:8765`);
|
||||||
|
|
||||||
ws.onopen = () => console.log("WebSocket Terhubung ke Pi!");
|
ws.onopen = () => console.log("WebSocket Terhubung ke Pi!");
|
||||||
ws.onclose = () => console.log("WebSocket Terputus.");
|
ws.onclose = () => console.log("WebSocket Terputus.");
|
||||||
wsRef.current = ws;
|
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(() => {
|
const interval = setInterval(() => {
|
||||||
|
let currentX = 0;
|
||||||
|
let currentY = 0;
|
||||||
|
|
||||||
|
if (controlMode === 'gamepad') {
|
||||||
if (typeof navigator !== "undefined" && navigator.getGamepads) {
|
if (typeof navigator !== "undefined" && navigator.getGamepads) {
|
||||||
const gamepads = navigator.getGamepads();
|
const gamepads = navigator.getGamepads();
|
||||||
let activeGp = null;
|
let activeGp = null;
|
||||||
@ -73,40 +70,56 @@ if (activeGp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeGp) {
|
if (activeGp) {
|
||||||
setGamepadStatus(`🟢 TERHUBUNG: ${activeGp.id.substring(0, 15)}...`);
|
|
||||||
const rawX = activeGp.axes[0];
|
const rawX = activeGp.axes[0];
|
||||||
const rawY = activeGp.axes[1];
|
const lt = activeGp.buttons[6].value;
|
||||||
|
const rt = activeGp.buttons[7].value;
|
||||||
|
|
||||||
const x = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0;
|
const throttle = rt - lt;
|
||||||
const y = Math.abs(rawY) > 0.15 ? Number(rawY.toFixed(2)) : 0;
|
currentX = Math.abs(rawX) > 0.15 ? Number(rawX.toFixed(2)) : 0;
|
||||||
|
currentY = Number(throttle.toFixed(2));
|
||||||
setSteering({ x, y });
|
|
||||||
|
|
||||||
// 3. KIRIM DATA KE RASPBERRY PI
|
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.send(JSON.stringify({ x, y }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSteering({ x: currentX, y: currentY });
|
||||||
|
|
||||||
|
// wsRef dipastikan aman dari TS error
|
||||||
|
if (wsRef.current && wsRef.current.readyState === 1) {
|
||||||
|
wsRef.current.send(JSON.stringify({ x: currentX, y: currentY }));
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
if (wsRef.current) wsRef.current.close();
|
if (wsRef.current) wsRef.current.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [controlMode, PI_IP]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-gray-900 text-white p-8 font-sans">
|
<main className="min-h-screen bg-gray-900 text-white p-8 font-sans">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<header className="mb-8 border-b border-gray-700 pb-4 flex justify-between items-center">
|
<header className="mb-8 border-b border-gray-700 pb-4 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<h1 className="text-3xl font-bold text-emerald-400">Robot Vision Control</h1>
|
<h1 className="text-3xl font-bold text-emerald-400">Robot Vision Control</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-2 bg-gray-800 p-1 rounded-lg border border-gray-700">
|
||||||
<button
|
<button
|
||||||
onClick={forceScanGamepad}
|
onClick={() => setControlMode('gamepad')}
|
||||||
className="bg-emerald-500 hover:bg-emerald-400 text-gray-900 font-bold py-2 px-6 rounded shadow-lg transition-transform active:scale-95"
|
className={`py-2 px-4 rounded font-semibold transition-colors ${controlMode === 'gamepad' ? 'bg-emerald-500 text-gray-900' : 'text-gray-400 hover:text-white'}`}
|
||||||
>
|
>
|
||||||
🕹️ Pindai Gamepad
|
🎮 Gamepad
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setControlMode('keyboard')}
|
||||||
|
className={`py-2 px-4 rounded font-semibold transition-colors ${controlMode === 'keyboard' ? 'bg-emerald-500 text-gray-900' : 'text-gray-400 hover:text-white'}`}
|
||||||
|
>
|
||||||
|
⌨️ Keyboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
@ -122,24 +135,32 @@ if (activeGp) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700 shadow-xl flex flex-col">
|
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700 shadow-xl flex flex-col">
|
||||||
<h2 className="text-lg font-semibold mb-4 border-b border-gray-700 pb-2">
|
<h2 className="text-lg font-semibold mb-4 border-b border-gray-700 pb-2 flex justify-between items-center">
|
||||||
XBOX Controller
|
<span>Status Kontrol</span>
|
||||||
|
{controlMode === 'gamepad' && (
|
||||||
|
<button onClick={forceScanGamepad} className="text-xs bg-gray-700 hover:bg-gray-600 py-1 px-2 rounded">
|
||||||
|
Pindai Ulang
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col justify-center gap-6">
|
<div className="flex-1 flex flex-col justify-center gap-6">
|
||||||
<div className="bg-gray-900 p-3 rounded border border-gray-700 text-sm font-mono text-center">
|
<div className="bg-gray-900 p-3 rounded border border-gray-700 text-sm font-mono text-center">
|
||||||
{gamepadStatus}
|
{controlMode === 'keyboard' ? '⌨️ AKTIF: Gunakan W-A-S-D atau Panah' : gamepadStatus}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-900 p-4 rounded-lg flex flex-col items-center gap-4 border border-gray-700">
|
<div className="bg-gray-900 p-4 rounded-lg flex flex-col items-center gap-4 border border-gray-700">
|
||||||
<div className="text-xs text-gray-400 text-center mb-2">Left Analog Stick</div>
|
<div className="text-xs text-gray-400 text-center mb-2">Visualizer (X / Y)</div>
|
||||||
<div className="w-24 h-24 bg-gray-800 rounded-full border-2 border-gray-600 relative">
|
<div className="w-24 h-24 bg-gray-800 rounded-full border-2 border-gray-600 relative overflow-hidden">
|
||||||
|
<div className="absolute w-full h-1px bg-gray-700 top-1/2 left-0"></div>
|
||||||
|
<div className="absolute h-full w-1px bg-gray-700 left-1/2 top-0"></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="w-6 h-6 bg-emerald-400 rounded-full absolute shadow-[0_0_10px_rgba(52,211,153,0.5)] transition-transform duration-75"
|
className="w-6 h-6 bg-emerald-400 rounded-full absolute shadow-[0_0_10px_rgba(52,211,153,0.5)] transition-all duration-75"
|
||||||
style={{
|
style={{
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: `translate(calc(-50% + ${steering.x * 40}px), calc(-50% + ${steering.y * 40}px))`
|
transform: `translate(calc(-50% + ${steering.x * 40}px), calc(-50% + ${-steering.y * 40}px))`
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user