mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webui</title>
|
||||
<script type="module" crossorigin src="/assets/index-CXibTtk4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BUE8ypH_.css">
|
||||
<script type="module" crossorigin src="/assets/index-0rsVlryQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-f8aVcpCN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ import { useVideoRequests } from './hooks/useVideoRequests.js';
|
||||
|
||||
function RoomCameraPanel() {
|
||||
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="mt-1 text-center text-sm text-slate-200">Feed placeholder. Wire upcoming room cam here.</p>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@ function useLayoutMode() {
|
||||
function LogPanel() {
|
||||
const { logs } = useSession();
|
||||
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">
|
||||
<span>Server logs</span>
|
||||
<span>{logs.length}</span>
|
||||
@@ -91,7 +91,7 @@ function DriverVideoPanel() {
|
||||
}, [session?.roster, roverId]);
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#1b1b1b] p-1">
|
||||
<section className="rounded-sm bg-[#242a32] p-1">
|
||||
{roverId ? (
|
||||
<div className="min-h-[55vh]">
|
||||
<VideoTile
|
||||
@@ -130,7 +130,7 @@ function AuthPanel() {
|
||||
}
|
||||
|
||||
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>
|
||||
<form className="mt-1 flex flex-col gap-1" onSubmit={handleLogin}>
|
||||
<input
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function AdminPanel() {
|
||||
if (!isAdmin) return null;
|
||||
|
||||
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">
|
||||
<span>Admin controls</span>
|
||||
<select
|
||||
|
||||
@@ -19,42 +19,32 @@ export default function DrivePanel() {
|
||||
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
||||
<span>Drive control</span>
|
||||
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
|
||||
</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={runStartDockFull}
|
||||
disabled={!roverId}
|
||||
className="w-full rounded-sm bg-black/50 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
|
||||
>
|
||||
Enable driving mode
|
||||
</button>
|
||||
<p className="mt-1 text-xs text-slate-400">Runs Start → Dock → Full to prep sensors.</p>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1 text-[0.65rem]">
|
||||
<StatusPill label="Driving" active={drivingMode} />
|
||||
{updated && <span className="text-slate-500">Sensors {updated}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={seekDock}
|
||||
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>
|
||||
<ActionCard
|
||||
title="Start Driving"
|
||||
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
|
||||
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
|
||||
gradient="from-emerald-600 to-emerald-400"
|
||||
onClick={runStartDockFull}
|
||||
disabled={!roverId}
|
||||
footnote={updated ? `Sensor ping ${updated}` : null}
|
||||
/>
|
||||
<ActionCard
|
||||
title="Dock and Charge"
|
||||
description="Line the rover up about a foot from the dock, then trigger an automatic approach."
|
||||
statuses={[
|
||||
{ label: docked ? 'Docked!' : 'Not Docked!', active: docked },
|
||||
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
|
||||
]}
|
||||
gradient="from-indigo-600 to-purple-500"
|
||||
onClick={seekDock}
|
||||
disabled={!roverId}
|
||||
/>
|
||||
<SpeedRow left={speeds.left} right={speeds.right} />
|
||||
<div>
|
||||
<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 }) {
|
||||
return (
|
||||
<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]);
|
||||
|
||||
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="mt-1 flex items-center justify-center">
|
||||
<Joystick
|
||||
@@ -79,7 +79,7 @@ function AuxMotorControls() {
|
||||
];
|
||||
|
||||
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="mt-1 flex flex-wrap gap-1">
|
||||
<button
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function TelemetryPanel() {
|
||||
}
|
||||
|
||||
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">
|
||||
<span>{connected ? 'online' : 'offline'}</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';
|
||||
|
||||
export function useVideoRequests(roverIds = []) {
|
||||
const socket = useSocket();
|
||||
const [sources, setSources] = useState({});
|
||||
const normalizedKey = Array.isArray(roverIds) ? roverIds.filter(Boolean).join('|') : '';
|
||||
|
||||
useEffect(() => {
|
||||
const ids = Array.from(new Set(roverIds.filter(Boolean)));
|
||||
if (!ids.length) {
|
||||
setSources({});
|
||||
return;
|
||||
if (!normalizedKey) {
|
||||
return undefined;
|
||||
}
|
||||
const ids = normalizedKey.split('|');
|
||||
let cancelled = false;
|
||||
setSources((prev) => {
|
||||
const next = {};
|
||||
ids.forEach((id) => {
|
||||
if (prev[id]) next[id] = prev[id];
|
||||
});
|
||||
return next;
|
||||
});
|
||||
ids.forEach((roverId) => {
|
||||
socket.emit('video:request', { roverId }, (resp = {}) => {
|
||||
if (cancelled) return;
|
||||
@@ -32,7 +25,19 @@ export function useVideoRequests(roverIds = []) {
|
||||
return () => {
|
||||
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