diff --git a/pi/bin/video-publisher.sh b/pi/bin/video-publisher.sh index 83e4cb11..dd7ad430 100644 --- a/pi/bin/video-publisher.sh +++ b/pi/bin/video-publisher.sh @@ -44,6 +44,7 @@ run_pipeline() { --timeout 0 \ --width "${VIDEO_WIDTH}" \ --height "${VIDEO_HEIGHT}" \ + --rotation 180 \ --framerate "${VIDEO_FPS}" \ --bitrate "${VIDEO_BITRATE}" \ --codec h264 \ diff --git a/plans/spectate_mode_overhaul_outline.md b/plans/spectate_mode_overhaul_outline.md index 5bd61c1b..c74636bf 100644 --- a/plans/spectate_mode_overhaul_outline.md +++ b/plans/spectate_mode_overhaul_outline.md @@ -1,5 +1,6 @@ # spectate system overhaul - rip out and remake the entire page + - basically keep nothing from it - right now the spectator page is a mess, and hasn't been updated in a while - does not work properly, aside from the page being out of date - the spectate page should use modules from the driver page @@ -32,6 +33,7 @@ ## layout and styling of the spectator page - use same styling style as the driver page +- feel free to add spectator role checks to hide the features that spectators can't use in certain components. - mainly designed for a 4:3 monitor - make use of the vertical space - a row of columns, one for each rover at the top diff --git a/server/src/services/roverManager.js b/server/src/services/roverManager.js index e6f1b6bc..27a29153 100644 --- a/server/src/services/roverManager.js +++ b/server/src/services/roverManager.js @@ -356,6 +356,10 @@ io.on('connection', (socket) => { cb({ error: 'Spectator role required' }); return; } + if (getMode() === MODES.LOCKDOWN) { + cb({ error: 'Spectating disabled in lockdown' }); + return; + } logger.info('Spectator subscribing to all rovers', socket.id); for (const record of rovers.values()) { socket.join(record.room); diff --git a/webui/src/hooks/useSpectatorMode.js b/webui/src/hooks/useSpectatorMode.js index 5e892b0c..19fc74c2 100644 --- a/webui/src/hooks/useSpectatorMode.js +++ b/webui/src/hooks/useSpectatorMode.js @@ -2,12 +2,16 @@ import { useEffect, useState } from 'react'; import { useSession } from '../context/SessionContext.jsx'; export function useSpectatorMode() { - const { session, setRole, subscribeAll } = useSession(); + const { session, setRole, subscribeAll, connected } = useSession(); const [ready, setReady] = useState(false); useEffect(() => { let cancelled = false; async function ensureSpectator() { + if (session?.mode === 'lockdown') { + setReady(false); + return; + } try { if (session?.role !== 'spectator') { await setRole('spectator'); @@ -18,13 +22,16 @@ export function useSpectatorMode() { } } catch (err) { console.error('Failed to enter spectator mode', err); + if (!cancelled) { + setReady(false); + } } } ensureSpectator(); return () => { cancelled = true; }; - }, [session?.role, setRole, subscribeAll]); + }, [connected, session?.mode, session?.role, setRole, subscribeAll]); return ready; } diff --git a/webui/src/spectate/SpectatorApp.jsx b/webui/src/spectate/SpectatorApp.jsx index 4d330feb..445979a7 100644 --- a/webui/src/spectate/SpectatorApp.jsx +++ b/webui/src/spectate/SpectatorApp.jsx @@ -1,11 +1,16 @@ import { useMemo } from 'react'; +import { SettingsProvider } from '../settings/index.js'; import { useSession } from '../context/SessionContext.jsx'; import { useSpectatorMode } from '../hooks/useSpectatorMode.js'; import { useTelemetryFrames } from '../context/TelemetryContext.jsx'; import { useVideoRequests } from '../hooks/useVideoRequests.js'; import VideoTile from '../components/VideoTile.jsx'; +import RoomCameraPanel from '../components/RoomCameraPanel.jsx'; +import UserListPanel from '../components/UserListPanel.jsx'; +import ChatPanel from '../components/ChatPanel.jsx'; +import LogPanel from '../components/LogPanel.jsx'; -function SpectatorStatus({ connected, ready, rosterCount }) { +function SpectatorStatus({ connected, ready, rosterCount, mode, blocked }) { return (
@@ -15,111 +20,135 @@ function SpectatorStatus({ connected, ready, rosterCount }) { Spectator mode {ready ? 'active' : 'pending…'} Rovers: {rosterCount} + Mode: {mode || 'unknown'} + {blocked && Spectate blocked}
); } -function LogStream({ logs }) { +function TelemetrySummary({ frame }) { + const sensors = frame?.sensors || {}; + const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null; + const entries = [ + ['Voltage', sensors.voltageMv != null ? `${(sensors.voltageMv / 1000).toFixed(2)} V` : '--'], + ['Current', sensors.currentMa != null ? `${sensors.currentMa} mA` : '--'], + ['Charge', sensors.batteryChargeMah != null ? `${sensors.batteryChargeMah}` : '--'], + ['OI', sensors.oiMode?.label || '--'], + ]; + return ( -
-
-

Server logs

- latest {logs.length} -
-
- {logs.length === 0 ? ( -

No log entries yet.

- ) : ( - logs - .slice() - .reverse() - .map((entry) => ( -
- {entry.timestamp}{' '} - [{entry.level}]{' '} - {entry.label && [{entry.label}]}{' '} - {entry.message} -
- )) - )} +
+
+ Telemetry + {updated ? `Updated ${updated}` : 'Waiting...'}
-
+
+ {entries.map(([label, value]) => ( +
+ {label} + {value} +
+ ))} +
+ ); } -function formatSensors(sensors = {}) { - return Object.entries(sensors).map(([key, value]) => ({ - key, - value: typeof value === 'object' ? JSON.stringify(value) : value, - })); +function CurrentDriverBadge({ roverId, session }) { + const activeDriverId = session?.activeDrivers?.[roverId] || null; + const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId); + const label = user?.nickname || (activeDriverId ? activeDriverId.slice(0, 6) : 'No driver'); + const mode = session?.mode; + const turnInfo = session?.turnQueues?.[roverId]; + const driverText = mode === 'turns' && turnInfo?.current ? `Driver: ${label} (turns)` : `Driver: ${label}`; + + return ( +
+ {driverText} +
+ ); } -function RoverSpectatorCard({ rover, frame, videoInfo }) { - const sensorList = formatSensors(frame?.sensors); +function RoverSpectatorCard({ rover, frame, videoInfo, session }) { return (
-
-

{rover.name}

-

- {frame?.receivedAt ? `Updated ${new Date(frame.receivedAt).toLocaleTimeString()}` : 'Waiting for telemetry…'} -

+
+

{rover.name}

+
- -
-

Telemetry

- {sensorList.length === 0 ? ( -

No sensor data yet.

- ) : ( -
- {sensorList.map(({ key, value }) => ( -
-
{key}
-
{value}
-
- ))} -
- )} -
+ +
); } +function RoverRow({ roster, frames, videoSources, session }) { + if (roster.length === 0) { + return

No rovers registered.

; + } + return ( +
+ {roster.map((rover) => ( + + ))} +
+ ); +} + +function SecondaryRow() { + return ( +
+ + + +
+ ); +} + +function LogsRow() { + return ( +
+ +
+ ); +} + export default function SpectatorApp() { - const { connected, session, logs } = useSession(); + const { connected, session } = useSession(); const spectatorReady = useSpectatorMode(); const frames = useTelemetryFrames(); const roster = session?.roster ?? []; const videoSources = useVideoRequests(roster.map((rover) => rover.id)); + const blocked = session?.mode === 'lockdown'; const status = useMemo( - () => ({ connected, ready: spectatorReady, rosterCount: roster.length }), - [connected, spectatorReady, roster.length], + () => ({ connected, ready: spectatorReady, rosterCount: roster.length, mode: session?.mode, blocked }), + [blocked, connected, roster.length, session?.mode, spectatorReady], ); return ( -
-
-
-

Spectator Console

-

Live rover telemetry & logs. This view is read-only.

- -
-
- {roster.length === 0 ? ( -

No rovers registered.

- ) : ( - roster.map((rover) => ( - - )) - )} -
- -
-
+ +
+
+
+

Spectator Console

+

Live rover telemetry & logs. This view is read-only.

+ + {blocked && ( +

Lockdown active. Spectator view will resume automatically when access returns.

+ )} +
+ + + +
+
+
); }