This commit is contained in:
legop3
2025-11-16 04:01:33 -05:00
parent 750c6401d2
commit 9b80c2f4c6
13 changed files with 204 additions and 92 deletions
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
+2 -2
View File
@@ -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-CAtTh0CL.js"></script> <script type="module" crossorigin src="/assets/index-CD08Z3yR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DU_3z6JA.css"> <link rel="stylesheet" crossorigin href="/assets/index-pos682hi.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1
View File
@@ -0,0 +1 @@
const io = require('../globals/io');
+12 -11
View File
@@ -9,6 +9,7 @@ import RoomCameraPanel from './components/RoomCameraPanel.jsx';
import LogPanel from './components/LogPanel.jsx'; import LogPanel from './components/LogPanel.jsx';
import AuthPanel from './components/AuthPanel.jsx'; import AuthPanel from './components/AuthPanel.jsx';
import DriverVideoPanel from './components/DriverVideoPanel.jsx'; import DriverVideoPanel from './components/DriverVideoPanel.jsx';
import RightPaneTabs from './components/RightPaneTabs.jsx';
function useLayoutMode() { function useLayoutMode() {
const [mode, setMode] = useState(() => { const [mode, setMode] = useState(() => {
@@ -40,20 +41,15 @@ function useLayoutMode() {
return mode; return mode;
} }
function DesktopLayout() { function DesktopLayout({ layout }) {
return ( return (
<div className="flex flex-col gap-1"> <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"> <section className="grid grid-cols-[minmax(0,1.8fr)_minmax(0,1fr)] gap-1">
<TelemetryPanel />
<DriverVideoPanel /> <DriverVideoPanel />
<DrivePanel /> <RightPaneTabs layout={layout} />
</section> </section>
<section className="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-1"> <section className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-1">
<div className="flex flex-col gap-1"> <AdminPanel />
<AuthPanel />
<AdminPanel />
</div>
<RoomCameraPanel />
<LogPanel /> <LogPanel />
</section> </section>
</div> </div>
@@ -105,7 +101,12 @@ function MobileLandscapeLayout() {
function App() { function App() {
const layout = useLayoutMode(); 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 ( return (
<div className="min-h-screen bg-black text-slate-50"> <div className="min-h-screen bg-black text-slate-50">
+74
View File
@@ -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 -64
View File
@@ -1,14 +1,8 @@
import { useDriveControl } from '../context/DriveControlContext.jsx'; import { useDriveControl } from '../context/DriveControlContext.jsx';
import { useTelemetryFrame } from '../context/TelemetryContext.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() { export default function DrivePanel() {
const { roverId, speeds, stopMotors, sendOiCommand, runStartDockFull, seekDock } = useDriveControl(); const { roverId, runStartDockFull, seekDock } = useDriveControl();
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
const drivingMode = (sensors.oiMode?.label || '').toLowerCase() === 'full'; const drivingMode = (sensors.oiMode?.label || '').toLowerCase() === 'full';
@@ -16,7 +10,6 @@ export default function DrivePanel() {
const charging = Boolean( const charging = Boolean(
sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging', sensors.chargingState?.label && sensors.chargingState.label.toLowerCase() !== 'not charging',
); );
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
return ( return (
<section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100"> <section className="rounded-sm bg-[#242a32] p-1 text-base text-slate-100">
@@ -32,7 +25,6 @@ export default function DrivePanel() {
tone="emerald" tone="emerald"
onClick={runStartDockFull} onClick={runStartDockFull}
disabled={!roverId} disabled={!roverId}
// footnote={updated ? `Sensor ping ${updated}` : null}
/> />
<ActionCard <ActionCard
title="Dock and Charge" title="Dock and Charge"
@@ -45,35 +37,7 @@ export default function DrivePanel() {
onClick={seekDock} onClick={seekDock}
disabled={!roverId} 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> </div>
{/* <p className="mt-1 text-sm text-slate-400">Sensor streaming auto-starts after each OI change.</p> */}
</section> </section>
); );
} }
@@ -105,30 +69,3 @@ function ActionCard({ title, description, statuses, tone, onClick, disabled, foo
</button> </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>
);
}
+1 -2
View File
@@ -18,8 +18,7 @@ export default function DriverVideoPanel() {
return ( return (
<section className="rounded-sm bg-[#242a32] p-1"> <section className="rounded-sm bg-[#242a32] p-1">
{roverId ? ( {roverId ? (
<div className=""> <div className="w-full">
{/* <div className="lg:min-h-[70vh]"> */}
<VideoTile <VideoTile
sessionInfo={info} sessionInfo={info}
label={roverId} label={roverId}
+32
View File
@@ -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>
);
}
+68
View File
@@ -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>
);
}
+1 -1
View File
@@ -130,7 +130,7 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
return ( return (
<div className="flex flex-col gap-1"> <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 <video
ref={videoRef} ref={videoRef}
muted={forceMute || muted} muted={forceMute || muted}