Robot Vision Control
++ ROBOT VISION +
+ {isRecordingUI && ( + + MEREKAM... + + )} + {isPlayingUI && ( + + MEMUTAR ULANG... + + )} +- - Live Feed -
-Pastikan Pi dan HP berada dalam jaringan Tailscale/Wi-Fi yang sama.
From 213d1aa5eb88cbd9e1ab1fc9ee53dbd7c00c834f Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Tue, 6 Oct 2026 18:42:39 +0700 Subject: [PATCH] . --- app/api/trajectory/route.js | 29 ++ app/layout.tsx | 18 +- app/page.tsx | 721 +++++++++++++++++++++++++----- next.config.ts => next.config.mjs | 4 +- perjalanan.json | 9 + tsconfig.json | 19 +- 6 files changed, 676 insertions(+), 124 deletions(-) create mode 100644 app/api/trajectory/route.js rename next.config.ts => next.config.mjs (60%) create mode 100644 perjalanan.json diff --git a/app/api/trajectory/route.js b/app/api/trajectory/route.js new file mode 100644 index 0000000..5912aa7 --- /dev/null +++ b/app/api/trajectory/route.js @@ -0,0 +1,29 @@ +import fs from 'fs'; +import path from 'path'; + +// File JSON akan disimpan di root direktori proyek Next.js +const filePath = path.join(process.cwd(), 'perjalanan.json'); + +// Menerima data dari Tombol Y (Save) +export async function POST(req) { + try { + const data = await req.json(); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + return Response.json({ success: true, message: 'Data perjalanan berhasil disimpan!' }); + } catch (error) { + return Response.json({ success: false, error: 'Gagal menyimpan data' }, { status: 500 }); + } +} + +// Membaca data untuk Tombol A & X (Playback) +export async function GET() { + try { + if (!fs.existsSync(filePath)) { + return Response.json([]); // Kembalikan array kosong jika belum ada rekaman + } + const fileData = fs.readFileSync(filePath, 'utf-8'); + return Response.json(JSON.parse(fileData)); + } catch (error) { + return Response.json({ success: false, error: 'Gagal membaca data' }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 976eb90..92d0809 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,20 +1,20 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter, Roboto_Mono } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const inter = Inter({ + variable: "--font-inter", subsets: ["latin"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const robotoMono = Roboto_Mono({ + variable: "--font-roboto-mono", subsets: ["latin"], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Robot UI", + description: "Robot Vision Control Dashboard", }; export default function RootLayout({ @@ -25,9 +25,9 @@ export default function RootLayout({ return (
{children} ); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 298c71e..750841b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,35 +2,289 @@ import { useState, useEffect, useRef } from 'react'; +// ============================================================ +// HELPER +// ============================================================ +const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +// ============================================================ +// KONFIGURASI +// ============================================================ +const RECORD_INTERVAL_MS = 50; +const DIRECTION_BRAKE_MS = 600; +const MAX_STEP_TIMEOUT_MS = 15000; +const LATENCY_COMPENSATION_TICKS = 3; + +// ============================================================ +// TYPE +// ============================================================ +type Steering = { + x: number; + y: number; +}; + +type MotionType = + | 'FORWARD' + | 'REVERSE' + | 'TURN_LEFT' + | 'TURN_RIGHT' + | 'STOP'; + +type TrajectoryStep = { + x: number; + y: number; + target_ticks_l: number; + target_ticks_r: number; + motion?: MotionType; + sample_ms?: number; +}; + export default function RobotDashboard() { - const PI_IP = '172.17.172.16'; + const PI_IP = '192.168.0.105'; const streamUrl = `http://${PI_IP}:8080/stream`; - const [controlMode, setControlMode] = useState('gamepad'); - const [gamepadStatus, setGamepadStatus] = useState("Menunggu Gamepad..."); + const [controlMode, setControlMode] = useState('touch'); + const [gamepadStatus, setGamepadStatus] = useState('Menunggu Gamepad...'); const [steering, setSteering] = useState({ x: 0, y: 0 }); + const [ticks, setTicks] = useState({ l: 0, r: 0 }); + const [isRecordingUI, setIsRecordingUI] = useState(false); + const [isPlayingUI, setIsPlayingUI] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); // State Fullscreen + + const currentTicksRef = useRef({ l: 0, r: 0 }); + const lastRecordedTicksRef = useRef({ l: 0, r: 0 }); + const activeMotionRef = useRef