mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -04:00
guh
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>webui</title>
|
<title>webui</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CXibTtk4.js"></script>
|
<script type="module" crossorigin src="/assets/index-0rsVlryQ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BUE8ypH_.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-f8aVcpCN.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ import { useVideoRequests } from './hooks/useVideoRequests.js';
|
|||||||
|
|
||||||
function RoomCameraPanel() {
|
function RoomCameraPanel() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[8rem] rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-200">
|
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
|
||||||
<p className="text-center text-xs text-slate-400">Room camera</p>
|
<p className="text-center text-xs text-slate-400">Room camera</p>
|
||||||
<p className="mt-1 text-center text-sm text-slate-200">Feed placeholder. Wire upcoming room cam here.</p>
|
<p className="mt-1 text-center text-sm text-slate-200">Feed placeholder. Wire upcoming room cam here.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +52,7 @@ function useLayoutMode() {
|
|||||||
function LogPanel() {
|
function LogPanel() {
|
||||||
const { logs } = useSession();
|
const { logs } = useSession();
|
||||||
return (
|
return (
|
||||||
<div className="rounded-sm bg-[#1b1b1b] p-1 text-xs text-slate-200">
|
<div className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-200">
|
||||||
<div className="flex items-center justify-between text-[0.7rem] text-slate-400">
|
<div className="flex items-center justify-between text-[0.7rem] text-slate-400">
|
||||||
<span>Server logs</span>
|
<span>Server logs</span>
|
||||||
<span>{logs.length}</span>
|
<span>{logs.length}</span>
|
||||||
@@ -91,7 +91,7 @@ function DriverVideoPanel() {
|
|||||||
}, [session?.roster, roverId]);
|
}, [session?.roster, roverId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#1b1b1b] p-1">
|
<section className="rounded-sm bg-[#242a32] p-1">
|
||||||
{roverId ? (
|
{roverId ? (
|
||||||
<div className="min-h-[55vh]">
|
<div className="min-h-[55vh]">
|
||||||
<VideoTile
|
<VideoTile
|
||||||
@@ -130,7 +130,7 @@ function AuthPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-200">
|
<div className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
|
||||||
<p className="text-xs text-slate-400">Admin login</p>
|
<p className="text-xs text-slate-400">Admin login</p>
|
||||||
<form className="mt-1 flex flex-col gap-1" onSubmit={handleLogin}>
|
<form className="mt-1 flex flex-col gap-1" onSubmit={handleLogin}>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function AdminPanel() {
|
|||||||
if (!isAdmin) return null;
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#1b1b1b] p-1 text-xs text-slate-200">
|
<section className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-200">
|
||||||
<div className="flex items-center justify-between gap-1">
|
<div className="flex items-center justify-between gap-1">
|
||||||
<span>Admin controls</span>
|
<span>Admin controls</span>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -19,42 +19,32 @@ export default function DrivePanel() {
|
|||||||
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
|
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
|
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||||
<span>Drive control</span>
|
<span>Drive control</span>
|
||||||
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
|
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 space-y-1">
|
<div className="mt-1 space-y-1">
|
||||||
<div>
|
<ActionCard
|
||||||
<button
|
title="Start Driving"
|
||||||
type="button"
|
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
|
||||||
onClick={runStartDockFull}
|
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
|
||||||
disabled={!roverId}
|
gradient="from-emerald-600 to-emerald-400"
|
||||||
className="w-full rounded-sm bg-black/50 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
|
onClick={runStartDockFull}
|
||||||
>
|
disabled={!roverId}
|
||||||
Enable driving mode
|
footnote={updated ? `Sensor ping ${updated}` : null}
|
||||||
</button>
|
/>
|
||||||
<p className="mt-1 text-xs text-slate-400">Runs Start → Dock → Full to prep sensors.</p>
|
<ActionCard
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-1 text-[0.65rem]">
|
title="Dock and Charge"
|
||||||
<StatusPill label="Driving" active={drivingMode} />
|
description="Line the rover up about a foot from the dock, then trigger an automatic approach."
|
||||||
{updated && <span className="text-slate-500">Sensors {updated}</span>}
|
statuses={[
|
||||||
</div>
|
{ label: docked ? 'Docked!' : 'Not Docked!', active: docked },
|
||||||
</div>
|
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
|
||||||
<div>
|
]}
|
||||||
<button
|
gradient="from-indigo-600 to-purple-500"
|
||||||
type="button"
|
onClick={seekDock}
|
||||||
onClick={seekDock}
|
disabled={!roverId}
|
||||||
disabled={!roverId}
|
/>
|
||||||
className="w-full rounded-sm bg-black/50 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
|
|
||||||
>
|
|
||||||
Seek dock
|
|
||||||
</button>
|
|
||||||
<p className="mt-1 text-xs text-slate-400">Point the rover straight at the dock, about one foot away.</p>
|
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
|
||||||
<StatusPill label={docked ? 'Docked' : 'Not docked'} active={docked} />
|
|
||||||
<StatusPill label={charging ? 'Charging' : 'Not charging'} active={charging} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SpeedRow left={speeds.left} right={speeds.right} />
|
<SpeedRow left={speeds.left} right={speeds.right} />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-400">Manual OI</p>
|
<p className="text-xs text-slate-400">Manual OI</p>
|
||||||
@@ -88,6 +78,33 @@ export default function DrivePanel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ActionCard({ title, description, statuses, gradient, onClick, disabled, footnote }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`w-full rounded-lg bg-gradient-to-r ${gradient} px-2 py-2 text-left text-white disabled:opacity-40`}
|
||||||
|
>
|
||||||
|
<p className="text-base font-semibold">{title}</p>
|
||||||
|
<p className="text-sm text-emerald-50/80">{description}</p>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
|
{statuses.map((status) => (
|
||||||
|
<span
|
||||||
|
key={status.label}
|
||||||
|
className={`rounded-full px-3 py-0.5 text-xs font-semibold ${
|
||||||
|
status.active ? 'bg-lime-300 text-emerald-900' : 'bg-emerald-900 text-emerald-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{footnote && <p className="mt-1 text-xs text-emerald-50/80">{footnote}</p>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SpeedRow({ left, right }) {
|
function SpeedRow({ left, right }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-1 rounded-sm bg-black/40 p-1 text-[0.7rem]">
|
<div className="grid grid-cols-2 gap-1 rounded-sm bg-black/40 p-1 text-[0.7rem]">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function MobileJoystick() {
|
|||||||
}, [disabled, driveWithVector]);
|
}, [disabled, driveWithVector]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
|
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||||
<div className="text-xs text-slate-400">Joystick</div>
|
<div className="text-xs text-slate-400">Joystick</div>
|
||||||
<div className="mt-1 flex items-center justify-center">
|
<div className="mt-1 flex items-center justify-center">
|
||||||
<Joystick
|
<Joystick
|
||||||
@@ -79,7 +79,7 @@ function AuxMotorControls() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
|
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||||
<div className="text-xs text-slate-400">Aux motors</div>
|
<div className="text-xs text-slate-400">Aux motors</div>
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function TelemetryPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-200">
|
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
|
||||||
<div className="text-[0.7rem] text-slate-400">
|
<div className="text-[0.7rem] text-slate-400">
|
||||||
<span>{connected ? 'online' : 'offline'}</span>
|
<span>{connected ? 'online' : 'offline'}</span>
|
||||||
<span> · role {session?.role || 'unknown'}</span>
|
<span> · role {session?.role || 'unknown'}</span>
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useSocket } from '../context/SocketContext.jsx';
|
import { useSocket } from '../context/SocketContext.jsx';
|
||||||
|
|
||||||
export function useVideoRequests(roverIds = []) {
|
export function useVideoRequests(roverIds = []) {
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
const [sources, setSources] = useState({});
|
const [sources, setSources] = useState({});
|
||||||
|
const normalizedKey = Array.isArray(roverIds) ? roverIds.filter(Boolean).join('|') : '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ids = Array.from(new Set(roverIds.filter(Boolean)));
|
if (!normalizedKey) {
|
||||||
if (!ids.length) {
|
return undefined;
|
||||||
setSources({});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const ids = normalizedKey.split('|');
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setSources((prev) => {
|
|
||||||
const next = {};
|
|
||||||
ids.forEach((id) => {
|
|
||||||
if (prev[id]) next[id] = prev[id];
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
ids.forEach((roverId) => {
|
ids.forEach((roverId) => {
|
||||||
socket.emit('video:request', { roverId }, (resp = {}) => {
|
socket.emit('video:request', { roverId }, (resp = {}) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -32,7 +25,19 @@ export function useVideoRequests(roverIds = []) {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [socket, roverIds.join('|')]);
|
}, [socket, normalizedKey]);
|
||||||
|
|
||||||
return sources;
|
const filtered = useMemo(() => {
|
||||||
|
if (!normalizedKey) return {};
|
||||||
|
const ids = normalizedKey.split('|');
|
||||||
|
const next = {};
|
||||||
|
ids.forEach((id) => {
|
||||||
|
if (sources[id]) {
|
||||||
|
next[id] = sources[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
}, [normalizedKey, sources]);
|
||||||
|
|
||||||
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user