mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
redoe
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-CAtTh0CL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DU_3z6JA.css">
|
||||
<script type="module" crossorigin src="/assets/index-CD08Z3yR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-pos682hi.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
const io = require('../globals/io');
|
||||
+12
-11
@@ -9,6 +9,7 @@ import RoomCameraPanel from './components/RoomCameraPanel.jsx';
|
||||
import LogPanel from './components/LogPanel.jsx';
|
||||
import AuthPanel from './components/AuthPanel.jsx';
|
||||
import DriverVideoPanel from './components/DriverVideoPanel.jsx';
|
||||
import RightPaneTabs from './components/RightPaneTabs.jsx';
|
||||
|
||||
function useLayoutMode() {
|
||||
const [mode, setMode] = useState(() => {
|
||||
@@ -40,20 +41,15 @@ function useLayoutMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
function DesktopLayout() {
|
||||
function DesktopLayout({ layout }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<section className="grid grid-cols-[minmax(0,0.6fr)_minmax(0,2.2fr)_minmax(0,0.6fr)] gap-1">
|
||||
<TelemetryPanel />
|
||||
<section className="grid grid-cols-[minmax(0,1.8fr)_minmax(0,1fr)] gap-1">
|
||||
<DriverVideoPanel />
|
||||
<DrivePanel />
|
||||
<RightPaneTabs layout={layout} />
|
||||
</section>
|
||||
<section className="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<AuthPanel />
|
||||
<AdminPanel />
|
||||
</div>
|
||||
<RoomCameraPanel />
|
||||
<section className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-1">
|
||||
<AdminPanel />
|
||||
<LogPanel />
|
||||
</section>
|
||||
</div>
|
||||
@@ -105,7 +101,12 @@ function MobileLandscapeLayout() {
|
||||
|
||||
function App() {
|
||||
const layout = useLayoutMode();
|
||||
const renderedLayout = layout === 'desktop' ? <DesktopLayout /> : layout === 'mobile-landscape' ? <MobileLandscapeLayout /> : <MobilePortraitLayout />;
|
||||
const renderedLayout =
|
||||
layout === 'desktop'
|
||||
? <DesktopLayout layout={layout} />
|
||||
: layout === 'mobile-landscape'
|
||||
? <MobileLandscapeLayout />
|
||||
: <MobilePortraitLayout />;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-slate-50">
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDriveControl } from '../context/DriveControlContext.jsx';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import AuthPanel from './AuthPanel.jsx';
|
||||
|
||||
const manualTabs = [
|
||||
{ key: 'start', label: 'Start OI' },
|
||||
{ key: 'safe', label: 'Safe' },
|
||||
{ key: 'full', label: 'Full' },
|
||||
{ key: 'passive', label: 'Passive' },
|
||||
{ key: 'dock', label: 'Dock' },
|
||||
];
|
||||
|
||||
export default function AdvancedSettings() {
|
||||
const { roverId, sendOiCommand } = useDriveControl();
|
||||
const socket = useSocket();
|
||||
const canControl = Boolean(roverId);
|
||||
|
||||
const sensorButtons = useMemo(
|
||||
() => [
|
||||
{ key: 'start', label: 'Enable stream', enable: true },
|
||||
{ key: 'stop', label: 'Disable stream', enable: false },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSensorToggle = (enable) => {
|
||||
if (!roverId) return;
|
||||
socket.emit('command', {
|
||||
roverId,
|
||||
type: 'sensorStream',
|
||||
data: { sensorStream: { enable } },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<p className="text-xs text-slate-400">Manual OI commands</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{manualTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => sendOiCommand(tab.key)}
|
||||
disabled={!canControl}
|
||||
className="rounded-sm bg-black/40 px-1 py-0.5 text-xs text-slate-200 disabled:opacity-30"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<p className="text-xs text-slate-400">Sensor stream</p>
|
||||
<div className="mt-1 flex gap-1">
|
||||
{sensorButtons.map((btn) => (
|
||||
<button
|
||||
key={btn.key}
|
||||
type="button"
|
||||
onClick={() => handleSensorToggle(btn.enable)}
|
||||
disabled={!canControl}
|
||||
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
|
||||
>
|
||||
{btn.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!canControl && <p className="mt-1 text-xs text-slate-500">Assign a rover to toggle streams.</p>}
|
||||
</section>
|
||||
<AuthPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useDriveControl } from '../context/DriveControlContext.jsx';
|
||||
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||
|
||||
const manualOiButtons = [
|
||||
{ key: 'safe', label: 'Safe' },
|
||||
{ key: 'passive', label: 'Passive' },
|
||||
{ key: 'full', label: 'Full' },
|
||||
];
|
||||
|
||||
export default function DrivePanel() {
|
||||
const { roverId, speeds, stopMotors, sendOiCommand, runStartDockFull, seekDock } = useDriveControl();
|
||||
const { roverId, runStartDockFull, seekDock } = useDriveControl();
|
||||
const frame = useTelemetryFrame(roverId);
|
||||
const sensors = frame?.sensors || {};
|
||||
const drivingMode = (sensors.oiMode?.label || '').toLowerCase() === 'full';
|
||||
@@ -16,7 +10,6 @@ export default function DrivePanel() {
|
||||
const charging = Boolean(
|
||||
sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging',
|
||||
);
|
||||
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
|
||||
@@ -32,7 +25,6 @@ export default function DrivePanel() {
|
||||
tone="emerald"
|
||||
onClick={runStartDockFull}
|
||||
disabled={!roverId}
|
||||
// footnote={updated ? `Sensor ping ${updated}` : null}
|
||||
/>
|
||||
<ActionCard
|
||||
title="Dock and Charge"
|
||||
@@ -45,35 +37,7 @@ export default function DrivePanel() {
|
||||
onClick={seekDock}
|
||||
disabled={!roverId}
|
||||
/>
|
||||
{/* <SpeedRow left={speeds.left} right={speeds.right} /> */}
|
||||
{/* <div>
|
||||
<p className="text-sm text-slate-300">Manual OI</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{manualOiButtons.map((btn) => (
|
||||
<button
|
||||
key={btn.key}
|
||||
type="button"
|
||||
onClick={() => sendOiCommand(btn.key)}
|
||||
disabled={!roverId}
|
||||
className="rounded-sm bg-black/40 px-1 py-0.5 text-sm text-slate-200 disabled:opacity-30"
|
||||
>
|
||||
{btn.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
{/* <div className="flex flex-wrap gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopMotors}
|
||||
disabled={!roverId}
|
||||
className="flex-1 rounded-sm bg-red-600/70 px-1 py-1 text-sm text-white disabled:opacity-40"
|
||||
>
|
||||
Stop motors
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* <p className="mt-1 text-sm text-slate-400">Sensor streaming auto-starts after each OI change.</p> */}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -105,30 +69,3 @@ function ActionCard({ title, description, statuses, tone, onClick, disabled, foo
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SpeedRow({ left, right }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-1 rounded-sm bg-black/40 p-1 text-base">
|
||||
<div>
|
||||
<p className="text-sm text-slate-300">Left</p>
|
||||
<p className="font-semibold text-slate-100">{left}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-300">Right</p>
|
||||
<p className="font-semibold text-slate-100">{right}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusPill({ label, active }) {
|
||||
return (
|
||||
<span
|
||||
className={`rounded-sm px-1 py-0.5 text-xs ${
|
||||
active ? 'bg-emerald-500/30 text-emerald-100' : 'bg-black/40 text-slate-500'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ export default function DriverVideoPanel() {
|
||||
return (
|
||||
<section className="rounded-sm bg-[#242a32] p-1">
|
||||
{roverId ? (
|
||||
<div className="">
|
||||
{/* <div className="lg:min-h-[70vh]"> */}
|
||||
<div className="w-full">
|
||||
<VideoTile
|
||||
sessionInfo={info}
|
||||
label={roverId}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export default function HelpPanel({ layout }) {
|
||||
const presets = {
|
||||
desktop: [
|
||||
'Use WASD + Shift to drive. Video must stay focused.',
|
||||
'Macros on the left prep the rover before moving.',
|
||||
'Switch tabs on the right for telemetry, room, and advanced tools.',
|
||||
],
|
||||
'mobile-portrait': [
|
||||
'Keep joystick centered when not driving.',
|
||||
'Auxiliary buttons sit beside the joystick – use them carefully.',
|
||||
'Scroll to access drive/telemetry if screen space is limited.',
|
||||
],
|
||||
'mobile-landscape': [
|
||||
'Joystick and video are side by side for game-style control.',
|
||||
'Rotate back to portrait if you need admin controls quickly.',
|
||||
'Tabs are desktop-only; mobile shows sections stacked.',
|
||||
],
|
||||
};
|
||||
const tips = presets[layout] || presets.desktop;
|
||||
return (
|
||||
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
|
||||
<p className="text-sm text-slate-400">Help</p>
|
||||
<ul className="mt-1 space-y-1 text-sm">
|
||||
{tips.map((tip) => (
|
||||
<li key={tip} className="rounded-sm bg-black/30 px-1 py-1 text-slate-200">
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from 'react';
|
||||
import TelemetryPanel from './TelemetryPanel.jsx';
|
||||
import DrivePanel from './DrivePanel.jsx';
|
||||
import RoomCameraPanel from './RoomCameraPanel.jsx';
|
||||
import AdvancedSettings from './AdvancedSettings.jsx';
|
||||
import HelpPanel from './HelpPanel.jsx';
|
||||
|
||||
function TabButton({ active, onClick, children }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex-1 rounded-sm px-1 py-1 text-xs font-semibold ${
|
||||
active ? 'bg-slate-100 text-slate-900' : 'bg-black/30 text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomControlsPlaceholder() {
|
||||
return (
|
||||
<div className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
||||
<p className="text-xs text-slate-400">Room controls</p>
|
||||
<p className="text-sm text-slate-200">Coming soon: lighting, docks, and environmental toggles.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RightPaneTabs({ layout }) {
|
||||
const [active, setActive] = useState('telemetry');
|
||||
|
||||
return (
|
||||
<section className="rounded-sm bg-[#1a1d23] p-1 text-base text-slate-100">
|
||||
<div className="flex gap-1">
|
||||
<TabButton active={active === 'telemetry'} onClick={() => setActive('telemetry')}>
|
||||
Telemetry
|
||||
</TabButton>
|
||||
<TabButton active={active === 'room'} onClick={() => setActive('room')}>
|
||||
Room
|
||||
</TabButton>
|
||||
<TabButton active={active === 'advanced'} onClick={() => setActive('advanced')}>
|
||||
Advanced
|
||||
</TabButton>
|
||||
<TabButton active={active === 'help'} onClick={() => setActive('help')}>
|
||||
Help
|
||||
</TabButton>
|
||||
</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{active === 'telemetry' && (
|
||||
<div className="space-y-1">
|
||||
<TelemetryPanel />
|
||||
<DrivePanel />
|
||||
</div>
|
||||
)}
|
||||
{active === 'room' && (
|
||||
<div className="space-y-1">
|
||||
<RoomCameraPanel />
|
||||
<RoomControlsPlaceholder />
|
||||
</div>
|
||||
)}
|
||||
{active === 'advanced' && <AdvancedSettings />}
|
||||
{active === 'help' && <HelpPanel layout={layout} />}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -130,7 +130,7 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="relative w-full overflow-hidden rounded-sm bg-black">
|
||||
<div className="relative w-full overflow-hidden rounded-sm bg-black aspect-video">
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted={forceMute || muted}
|
||||
|
||||
Reference in New Issue
Block a user