mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
guh
This commit is contained in:
+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