mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
Compare commits
2
Commits
5ab62c3633
...
c8836f7d65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8836f7d65 | ||
|
|
f77a969098 |
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
@@ -12,8 +12,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-C0lpCbci.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BITzjVQo.css">
|
||||
<script type="module" crossorigin src="/assets/index-BZ2ymoHR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BFNKIMjg.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -97,8 +97,20 @@ roverManager.managerEvents.on('private', ({ roverId, open }) => {
|
||||
}
|
||||
});
|
||||
|
||||
roverManager.managerEvents.on('rover', ({ action }) => {
|
||||
if (action === 'removed' || action === 'upsert') {
|
||||
roverManager.managerEvents.on('rover', ({ roverId, action }) => {
|
||||
if (action === 'removed') {
|
||||
/*
|
||||
The physical rover record is the authority for current driver ownership.
|
||||
Once it disappears, every assignment that names it must be released and
|
||||
run through ordinary placement again. Leaving those map entries intact
|
||||
lets the same id become visible after reconnect without recreating its
|
||||
driver membership, which is the exact stale-UI/video-auth split this
|
||||
lifecycle boundary must prevent.
|
||||
*/
|
||||
reassignFromRover(roverId);
|
||||
return;
|
||||
}
|
||||
if (action === 'upsert') {
|
||||
reassignWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ const {
|
||||
} = privateAccess;
|
||||
|
||||
const roverLifecycle = createRoverLifecycle({
|
||||
io,
|
||||
rovers,
|
||||
socketToRovers,
|
||||
managerEvents,
|
||||
@@ -86,6 +87,7 @@ const roverLifecycle = createRoverLifecycle({
|
||||
const {
|
||||
requestControl,
|
||||
releaseControl,
|
||||
removeRoverDrivers,
|
||||
isDriver,
|
||||
canDrive,
|
||||
getRoversForSocket,
|
||||
@@ -119,6 +121,7 @@ const rosterLifecycle = createRosterLifecycle({
|
||||
normalizePrivateSafety,
|
||||
stopDockGuard: (...args) => stopDockGuard(...args),
|
||||
getControlDenialReason,
|
||||
removeRoverDrivers,
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@@ -24,6 +24,7 @@ function createRosterLifecycle(deps) {
|
||||
isRoverVisibleToSocket,
|
||||
normalizePrivateSafety,
|
||||
stopDockGuard,
|
||||
removeRoverDrivers,
|
||||
} = deps;
|
||||
|
||||
function ensureRecord(id) {
|
||||
@@ -106,7 +107,14 @@ function createRosterLifecycle(deps) {
|
||||
function removeRover(id) {
|
||||
const record = rovers.get(id);
|
||||
if (!record) return;
|
||||
/*
|
||||
Remove the public record before emitting driver-removal events. Any
|
||||
session sync caused by those events must already see this rover as
|
||||
offline, while removeRoverDrivers still receives the captured record so
|
||||
it can clean the reverse membership index and Socket.IO room membership.
|
||||
*/
|
||||
rovers.delete(id);
|
||||
removeRoverDrivers(id, record);
|
||||
stopDockGuard(id);
|
||||
privateButtonStates.delete(id);
|
||||
privateNoUsersSince.delete(id);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Scope: Keeps runtime behavior unchanged by reusing rover-manager state maps and injected policy helpers.
|
||||
function createRoverLifecycle(deps) {
|
||||
const {
|
||||
io,
|
||||
rovers,
|
||||
socketToRovers,
|
||||
managerEvents,
|
||||
@@ -85,6 +86,40 @@ function createRoverLifecycle(deps) {
|
||||
managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'remove' });
|
||||
}
|
||||
|
||||
function removeRoverDrivers(roverId, removedRecord = null) {
|
||||
/*
|
||||
A rover connection owns the record that contains its driver set, but the
|
||||
reverse socket-to-rover index outlives that record. Disconnect cleanup
|
||||
must therefore remove both halves before a reconnect creates a fresh
|
||||
record with the same id. Otherwise session assignment can name the rover
|
||||
while video/control authorization correctly sees no driver membership.
|
||||
|
||||
removedRecord is accepted because rosterLifecycle deliberately deletes
|
||||
the public rover record first. Session syncs triggered by the driver
|
||||
events below will consequently hide the unavailable rover immediately,
|
||||
even before assignmentService finishes normal reassignment.
|
||||
*/
|
||||
const record = removedRecord || rovers.get(roverId);
|
||||
if (!record) return [];
|
||||
const driverIds = Array.from(record.drivers || []);
|
||||
|
||||
driverIds.forEach((socketId) => {
|
||||
const joined = socketToRovers.get(socketId);
|
||||
if (joined) {
|
||||
joined.delete(roverId);
|
||||
if (joined.size === 0) socketToRovers.delete(socketId);
|
||||
}
|
||||
|
||||
const socket = io.sockets.sockets.get(socketId);
|
||||
socket?.leave(record.room);
|
||||
record.drivers.delete(socketId);
|
||||
turnService.driverRemoved(roverId, socketId);
|
||||
managerEvents.emit('driver', { socketId, roverId, action: 'remove' });
|
||||
});
|
||||
|
||||
return driverIds;
|
||||
}
|
||||
|
||||
function isDriver(roverId, socket) {
|
||||
const record = rovers.get(roverId);
|
||||
if (!record) return false;
|
||||
@@ -175,6 +210,7 @@ function createRoverLifecycle(deps) {
|
||||
removeSocket,
|
||||
requestControl,
|
||||
releaseControl,
|
||||
removeRoverDrivers,
|
||||
isDriver,
|
||||
canDrive,
|
||||
getRoversForSocket,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Rover Manager Lifecycle Tests
|
||||
// Purpose: Verifies that physical rover removal clears every ownership index before a same-id reconnect.
|
||||
// Scope: Covers driver membership cleanup only; assignment placement policy remains in assignmentService.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const EventEmitter = require('node:events');
|
||||
const { createRoverLifecycle } = require('./roverLifecycle');
|
||||
|
||||
test('removing a rover clears driver sets, reverse membership, rooms, and turns', () => {
|
||||
const roverId = 'rover-one';
|
||||
const socketId = 'driver-one';
|
||||
const leftRooms = [];
|
||||
const removedTurns = [];
|
||||
const driverEvents = [];
|
||||
const socket = {
|
||||
id: socketId,
|
||||
leave: (room) => leftRooms.push(room),
|
||||
};
|
||||
const record = {
|
||||
id: roverId,
|
||||
room: `rover:${roverId}`,
|
||||
drivers: new Set([socketId]),
|
||||
};
|
||||
const rovers = new Map([[roverId, record]]);
|
||||
const socketToRovers = new Map([[socketId, new Set([roverId])]]);
|
||||
const managerEvents = new EventEmitter();
|
||||
managerEvents.on('driver', (event) => driverEvents.push(event));
|
||||
|
||||
const lifecycle = createRoverLifecycle({
|
||||
io: { sockets: { sockets: new Map([[socketId, socket]]) } },
|
||||
rovers,
|
||||
socketToRovers,
|
||||
managerEvents,
|
||||
turnService: {
|
||||
driverRemoved: (removedRoverId, removedSocketId) => {
|
||||
removedTurns.push([removedRoverId, removedSocketId]);
|
||||
},
|
||||
},
|
||||
isAdmin: () => false,
|
||||
sendAlert: () => {},
|
||||
ALERT_COLOR: '#000000',
|
||||
getMode: () => 'public',
|
||||
getControlDenialReason: () => null,
|
||||
});
|
||||
|
||||
/* Mirror rosterLifecycle's ordering: the public record is gone before the
|
||||
captured record is supplied for complete membership cleanup. */
|
||||
rovers.delete(roverId);
|
||||
const removedDriverIds = lifecycle.removeRoverDrivers(roverId, record);
|
||||
|
||||
assert.deepEqual(removedDriverIds, [socketId]);
|
||||
assert.equal(record.drivers.size, 0);
|
||||
assert.equal(socketToRovers.has(socketId), false);
|
||||
assert.deepEqual(leftRooms, [`rover:${roverId}`]);
|
||||
assert.deepEqual(removedTurns, [[roverId, socketId]]);
|
||||
assert.deepEqual(driverEvents, [
|
||||
{ socketId, roverId, action: 'remove' },
|
||||
]);
|
||||
});
|
||||
@@ -135,6 +135,20 @@ function buildUserEntry(socket) {
|
||||
const role = getRole(socket);
|
||||
const assignment = assignmentService.describeAssignment(socket.id);
|
||||
const primaryRover = roverManager.getPrimaryRoverForSocket(socket.id);
|
||||
/*
|
||||
assignmentService owns automatic placement policy, while roverManager owns
|
||||
actual control membership. Validate both candidate indexes before exposing
|
||||
presence because neither cached direction is authoritative without the
|
||||
physical rover record agreeing that this socket is one of its drivers.
|
||||
*/
|
||||
const verifiedPrimaryRover = primaryRover
|
||||
&& roverManager.isDriver(primaryRover, socket)
|
||||
? primaryRover
|
||||
: null;
|
||||
const verifiedAssignmentRover = assignment?.roverId
|
||||
&& roverManager.isDriver(assignment.roverId, socket)
|
||||
? assignment.roverId
|
||||
: null;
|
||||
const ptzChatTarget = getPtzChatTargetForSocket(socket.id);
|
||||
return {
|
||||
socketId: socket.id,
|
||||
@@ -147,7 +161,7 @@ function buildUserEntry(socket) {
|
||||
the PTZ chat target while the socket is queued or operating so presence,
|
||||
queue lookup, and chat identity all agree.
|
||||
*/
|
||||
roverId: ptzChatTarget?.roverId || primaryRover || assignment?.roverId || null,
|
||||
roverId: ptzChatTarget?.roverId || verifiedPrimaryRover || verifiedAssignmentRover || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,7 +186,17 @@ function buildSession(socket) {
|
||||
}));
|
||||
const roster = roverManager.getRosterForSocket(socket);
|
||||
const assignment = assignmentService.describeAssignment(socket?.id || '');
|
||||
const assignmentRoverId = filterVisibleRoverId(socket, assignment?.roverId);
|
||||
/*
|
||||
Visibility alone is insufficient here: a reconnected rover can be visible
|
||||
before a stale assignment map has recreated actual driver membership. The
|
||||
session contract consumed by every UI surface must require both visibility
|
||||
and roverManager's authoritative membership check.
|
||||
*/
|
||||
const verifiedAssignmentRover = assignment?.roverId
|
||||
&& roverManager.isDriver(assignment.roverId, socket)
|
||||
? assignment.roverId
|
||||
: null;
|
||||
const assignmentRoverId = filterVisibleRoverId(socket, verifiedAssignmentRover);
|
||||
const activeDrivers = filterActiveDriversForSocket(getActiveDrivers(), socket);
|
||||
const turnQueues = filterTurnQueuesForSocket(getTurnQueues(), socket);
|
||||
const socials = features.socials && configuredSocials?.length ? configuredSocials : [];
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.8 MiB |
@@ -18,42 +18,69 @@ import ExpansionPanel from '../CornerPods/ExpansionPanel.jsx';
|
||||
import usePodVisibility from '../CornerPods/usePodVisibility.js';
|
||||
|
||||
function DockedAction({ driveKeyLabel, pending, controlsDisabled, error, onUndock }) {
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const waitingForTurn = controlsDisabled && !pending;
|
||||
|
||||
/* The dismissal belongs to this mounted docked episode. DockingHud unmounts
|
||||
this component when the rover leaves the base, and its roverId key remounts
|
||||
it for a different assignment, so no persistence or reset effect is needed. */
|
||||
if (hidden && !pending) return null;
|
||||
|
||||
const mainToneClass = waitingForTurn
|
||||
? 'cursor-not-allowed bg-slate-950/95 ring-slate-400/70'
|
||||
: 'bg-emerald-950/90 ring-emerald-300/80 hover:bg-emerald-900/95 focus-visible:ring-emerald-200 disabled:cursor-wait disabled:opacity-75';
|
||||
const hideToneClass = waitingForTurn
|
||||
? 'bg-slate-950/95 ring-slate-400/70 hover:bg-slate-900'
|
||||
: 'bg-emerald-950/90 ring-emerald-300/80 hover:bg-emerald-900/95 focus-visible:ring-emerald-200';
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center p-6">
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending || controlsDisabled}
|
||||
onClick={onUndock}
|
||||
className={`pointer-events-auto flex w-[min(32rem,80%)] flex-col items-center gap-2 px-8 py-7 text-center text-white shadow-2xl ring-2 transition focus-visible:outline-none focus-visible:ring-4 ${
|
||||
waitingForTurn
|
||||
? 'cursor-not-allowed bg-slate-950/95 ring-slate-400/70'
|
||||
: 'bg-emerald-950/90 ring-emerald-300/80 hover:bg-emerald-900/95 focus-visible:ring-emerald-200 disabled:cursor-wait disabled:opacity-75'
|
||||
}`}
|
||||
>
|
||||
<strong className="text-3xl leading-tight">{pending ? 'Undocking…' : 'Your rover is docked'}</strong>
|
||||
{pending ? (
|
||||
null
|
||||
) : waitingForTurn ? (
|
||||
/* A disabled action must explain the ownership constraint instead of
|
||||
continuing to advertise a click and keybind that cannot succeed. */
|
||||
<span className="text-lg font-semibold leading-snug text-slate-300">
|
||||
Wait for your turn to undock.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-lg font-semibold leading-snug text-emerald-50">
|
||||
Click here
|
||||
{driveKeyLabel ? (
|
||||
<>
|
||||
{' '}or press <KeyPill label={driveKeyLabel} />
|
||||
</>
|
||||
) : null}
|
||||
{' '}to undock and drive the rover
|
||||
</span>
|
||||
)}
|
||||
{error ? <span className="text-sm font-semibold text-red-200">{error}</span> : null}
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
{/* The docked shield is owned by the dismissible action so hiding the
|
||||
prompt also reveals the video and ordinary HUD instead of leaving an
|
||||
unexplained dark, input-blocking layer behind. */}
|
||||
<div className="pointer-events-auto absolute inset-0 z-[25] bg-black/75" aria-hidden="true" />
|
||||
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center p-6">
|
||||
<div className="relative w-[min(32rem,80%)]">
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending || controlsDisabled}
|
||||
onClick={onUndock}
|
||||
className={`pointer-events-auto flex w-full flex-col items-center gap-2 px-8 py-7 text-center text-white shadow-2xl ring-2 transition focus-visible:outline-none focus-visible:ring-4 ${mainToneClass}`}
|
||||
>
|
||||
<strong className="text-3xl leading-tight">{pending ? 'Undocking…' : 'Your rover is docked'}</strong>
|
||||
{pending ? (
|
||||
null
|
||||
) : waitingForTurn ? (
|
||||
/* A disabled action must explain the ownership constraint instead of
|
||||
continuing to advertise a click and keybind that cannot succeed. */
|
||||
<span className="text-lg font-semibold leading-snug text-slate-300">
|
||||
Wait for your turn to undock.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-lg font-semibold leading-snug text-emerald-50">
|
||||
Click here
|
||||
{driveKeyLabel ? (
|
||||
<>
|
||||
{' '}or press <KeyPill label={driveKeyLabel} />
|
||||
</>
|
||||
) : null}
|
||||
{' '}to undock and drive the rover
|
||||
</span>
|
||||
)}
|
||||
{error ? <span className="text-sm font-semibold text-red-200">{error}</span> : null}
|
||||
</button>
|
||||
{!pending ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHidden(true)}
|
||||
className={`pointer-events-auto absolute left-1/2 top-full -translate-x-1/2 rounded-b-lg px-6 py-1.5 text-sm font-bold text-white shadow-xl ring-2 transition focus-visible:outline-none focus-visible:ring-4 ${hideToneClass}`}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,15 +345,12 @@ export default function DockingHud({ roverId }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* This single layer both dims and blocks the ordinary rover HUD. A passive
|
||||
undocked rover is still moving autonomously, so it gets a lighter but equally
|
||||
blocking shield. Explicit pending state keeps the correct shield mounted until
|
||||
the complete drive sequence finishes even as telemetry changes underneath it. */}
|
||||
{/* Automatic docking keeps its own lighter blocking shield. The ordinary
|
||||
docked shield lives inside DockedAction because the new Hide control
|
||||
must dismiss the prompt and its dimming as one coherent surface. */}
|
||||
<div
|
||||
className={`absolute inset-0 z-[25] transition-all duration-300 ${
|
||||
docked || pendingAction === 'undocking'
|
||||
? 'pointer-events-auto bg-black/75 opacity-100'
|
||||
: autoDocking || pendingAction === 'resuming'
|
||||
autoDocking || pendingAction === 'resuming'
|
||||
? 'pointer-events-auto bg-black/55 opacity-100'
|
||||
: 'pointer-events-none opacity-0'
|
||||
}`}
|
||||
@@ -335,6 +359,7 @@ export default function DockingHud({ roverId }) {
|
||||
|
||||
{docked || pendingAction === 'undocking' ? (
|
||||
<DockedAction
|
||||
key={roverId}
|
||||
driveKeyLabel={driveKeyLabel}
|
||||
pending={pendingAction === 'undocking'}
|
||||
controlsDisabled={!canControl}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Initial Session Overlay
|
||||
// Purpose: Hides incomplete driver-page placeholders until the first authoritative session snapshot arrives.
|
||||
// Scope: Provides startup presentation only; it deliberately does not delay or alter page initialization underneath.
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import spinnerImage from '../../assets/spinner.png';
|
||||
import './styles.css';
|
||||
|
||||
export default function InitialSessionOverlay() {
|
||||
const connected = useSessionSelector((state) => Boolean(state.connected));
|
||||
const sessionReady = useSessionSelector((state) => state.session !== null);
|
||||
|
||||
if (sessionReady) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[1000] flex items-center justify-center bg-black text-slate-200"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={connected ? 'Loading session' : 'Connecting'}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{/* The source image is intentionally constrained to a small fixed box;
|
||||
its intrinsic pixel dimensions must never determine overlay layout.
|
||||
Reduced-motion users still see the identifying image without either
|
||||
the rotation or continuously changing color. */}
|
||||
<div className="initial-session-spinner-rotation h-20 w-20" aria-hidden="true">
|
||||
<img
|
||||
src={spinnerImage}
|
||||
alt=""
|
||||
draggable="false"
|
||||
className="initial-session-spinner-image h-full w-full select-none object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-300">
|
||||
{connected ? 'Loading session…' : 'Connecting…'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
These animations belong only to InitialSessionOverlay. Keeping them beside
|
||||
the component avoids adding feature-specific behavior to the global styles.
|
||||
Rotation and hue use separate elements so their independent infinite cycles
|
||||
cannot overwrite one another's transform or filter properties.
|
||||
*/
|
||||
@keyframes initial-session-spinner-rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes initial-session-spinner-hue {
|
||||
from {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
filter: hue-rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.initial-session-spinner-rotation {
|
||||
animation: initial-session-spinner-rotate 1.8s linear infinite;
|
||||
}
|
||||
|
||||
.initial-session-spinner-image {
|
||||
animation: initial-session-spinner-hue 2.8s linear infinite;
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,9 @@ function ControlRow({ label, keyLabel }) {
|
||||
function DesktopQuickstart({ keymap }) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200">1. Press "Start Driving" put your rover into driving mode.</p>
|
||||
<p className="text-sm text-slate-200">1. Click "Your rover is docked" to undock.</p>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm text-slate-200">2. Use the drive controls to move your rover:</p>
|
||||
<p className="text-sm text-slate-200">2. Drive with these keybindings:</p>
|
||||
<div className="space-y-0.5">
|
||||
<ControlRow label="Forward" keyLabel={formatKeyLabel(keymap?.driveForward?.[0])} />
|
||||
<ControlRow label="Backward" keyLabel={formatKeyLabel(keymap?.driveBackward?.[0])} />
|
||||
@@ -31,7 +31,8 @@ function DesktopQuickstart({ keymap }) {
|
||||
<ControlRow label="Move slower" keyLabel={formatKeyLabel(keymap?.slowModifier?.[0])} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-200">3. When done, enter "Docking Assist" and line up the front sensor with the dock sensor.</p>
|
||||
<p className="text-sm text-slate-200">3. Use the video HUD for rover controls and information.</p>
|
||||
<p className="text-sm text-slate-200">4. Click "Dock rover" when finished.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,10 +40,11 @@ function DesktopQuickstart({ keymap }) {
|
||||
function MobileQuickstart() {
|
||||
return (
|
||||
<div className="space-y-0.5 text-sm text-slate-200">
|
||||
<p>1. Press "Start Driving" put your rover into driving mode.</p>
|
||||
<p>2. Touch and hold in Joystick area to move.</p>
|
||||
<p>3. Use the other column for motor, horn, and camera controls.</p>
|
||||
<p>4. When done, enter "Docking Assist" and line up the front sensor with the dock sensor.</p>
|
||||
<p>1. Tap "Your rover is docked" to undock.</p>
|
||||
<p>2. Hold and drag on the drive pad.</p>
|
||||
<p>3. Choose Precision, Normal, or Turbo above the drive pad.</p>
|
||||
<p>4. Use the other column for rover controls.</p>
|
||||
<p>5. Tap "Dock and charge" when finished.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -71,7 +73,6 @@ export default function QuickstartOverlay({
|
||||
layout,
|
||||
showOnLoad,
|
||||
onToggleShowOnLoad,
|
||||
onOpenHelp,
|
||||
onClose,
|
||||
}) {
|
||||
const rawKeymap = useControlSelector((control) => control.state.keymap);
|
||||
|
||||
@@ -87,10 +87,6 @@ function DriverPageContent({ layout, oldDesktop }) {
|
||||
},
|
||||
[saveQuickstartSettings],
|
||||
);
|
||||
const openHelpFromQuickstart = useCallback(() => {
|
||||
setQuickstartVisible(false);
|
||||
setHelpVisible(true);
|
||||
}, []);
|
||||
return (
|
||||
<ControlSystemProvider>
|
||||
{/*
|
||||
@@ -127,7 +123,6 @@ function DriverPageContent({ layout, oldDesktop }) {
|
||||
layout={layout}
|
||||
showOnLoad={quickstartSettings?.showOnLoad !== false}
|
||||
onToggleShowOnLoad={setQuickstartShowOnLoad}
|
||||
onOpenHelp={openHelpFromQuickstart}
|
||||
onClose={closeQuickstart}
|
||||
/>
|
||||
</ControlSystemProvider>
|
||||
|
||||
+60
-27
@@ -11,26 +11,44 @@ export const HELP_CONTENT = {
|
||||
type: 'list',
|
||||
title: 'Chat and nicknames',
|
||||
items: [
|
||||
'Set a nickname in the user list panel, on the bottom left of the page below the rover video.',
|
||||
{ segments: ['Toggle chat focus with ', { action: 'chatFocus' }, '. Press ', { action: 'chatFocus'}, ' again to send.'] },
|
||||
'Chat and nickname controls are in the Chat/Rovers tab.',
|
||||
{ segments: ['Open the HUD chat composer with ', { action: 'chatFocus' }, '. Press ', { action: 'chatFocus'}, ' again to send.'] },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Driving the rover',
|
||||
items: [
|
||||
{ segments: ['Press the "Start Driving" button onscreen, or press ' , { action: 'driveMacro' }, ' on your keyboard to put the rover into driving mode.'] },
|
||||
'Refer to the controls for the controls for the rover.'
|
||||
{ segments: ['Click "Your rover is docked", or press ', { action: 'driveMacro' }, ', to undock.'] },
|
||||
'Drive with the movement keybindings.',
|
||||
'Rover controls dim while another person has the turn.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Video HUD',
|
||||
items: [
|
||||
'Top left shows the rover name and turns.',
|
||||
'Top right shows battery and rover status.',
|
||||
'Bottom left contains horn, headlight, and laser controls.',
|
||||
'Bottom right contains camera tilt and chat.',
|
||||
'Use the arrows to open and close pods and their expansions.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Page layout',
|
||||
items: [
|
||||
'Room, Activities, and VIP are in the left sidebar.',
|
||||
'Chat/Rovers, Help, and Settings are in the right sidebar.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Docking the rover',
|
||||
items: [
|
||||
'If the rover shows "Docking in Progress", it is already auto-seeking the dock.',
|
||||
'To dock manually, enter Docking Assist from the drive panel.',
|
||||
{ segments: ['Press "Enter Docking Assist", or press ', { action: 'dockMacro' }, '.'] },
|
||||
'In assist mode, camera tilts down and driving speed is limited for precise alignment.',
|
||||
{ segments: ['Click "Dock rover", or press ', { action: 'dockMacro' }, '.'] },
|
||||
'Click "Rover is docking itself" to resume driving.',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -56,8 +74,8 @@ export const HELP_CONTENT = {
|
||||
id: 'macros',
|
||||
title: 'Rover modes & chat',
|
||||
items: [
|
||||
{ action: 'driveMacro', label: 'Drive macro' },
|
||||
{ action: 'dockMacro', label: 'Docking assist toggle' },
|
||||
{ action: 'driveMacro', label: 'Undock / resume driving' },
|
||||
{ action: 'dockMacro', label: 'Dock rover' },
|
||||
{ action: 'chatFocus', label: 'Chat focus' },
|
||||
],
|
||||
},
|
||||
@@ -119,27 +137,35 @@ export const HELP_CONTENT = {
|
||||
type: 'list',
|
||||
title: 'Chat and nicknames',
|
||||
items: [
|
||||
'Set a nickname in the user list panel below.',
|
||||
'Tap in the chat box to send messages in the chat.'
|
||||
'Chat and nickname controls are in the Chat tab.',
|
||||
'Tap the chat box to send a message.',
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Driving the rover',
|
||||
items: [
|
||||
{ segments: ['Press the "Start Driving" button onscreen to put the rover into driving mode.'] },
|
||||
'Look below the rover video. Use the joystick column to move the rover, and hold the aux buttons in the other control column.'
|
||||
'Tap "Your rover is docked" to undock.',
|
||||
'Hold and drag on the drive pad.',
|
||||
'Choose Precision, Normal, or Turbo above the drive pad.',
|
||||
'Use the other column for rover controls.',
|
||||
'Rover controls dim while another person has the turn.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Docking the rover',
|
||||
items: [
|
||||
'If you see "Docking in Progress", the rover is currently auto-seeking the dock.',
|
||||
{ segments: ['For manual docking, press "Enter Docking Assist".'] },
|
||||
'Assist mode tilts camera down and limits speed for precise alignment.',
|
||||
'Tap "Dock and charge" when finished.',
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'More controls',
|
||||
items: [
|
||||
'Chat, Activities, VIP, Room Controls, Help, and Settings are below the rover controls.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'mobile-landscape': {
|
||||
@@ -148,28 +174,35 @@ export const HELP_CONTENT = {
|
||||
type: 'list',
|
||||
title: 'Chat and nicknames',
|
||||
items: [
|
||||
'Scroll down to see more of the page.',
|
||||
'Set a nickname in the user list panel below.',
|
||||
'Tap in the chat box to send messages in the chat.'
|
||||
'Chat and nickname controls are in the Chat tab.',
|
||||
'Tap the chat box to send a message.',
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Driving the rover',
|
||||
items: [
|
||||
{ segments: ['Press the "Start Driving" button, or the "Drive" button to put the rover into driving mode.'] },
|
||||
'Use the joystick column beside the video feed to move the rover, and hold the aux buttons in the other control column.'
|
||||
'Tap "Your rover is docked" to undock.',
|
||||
'Hold and drag on the drive pad.',
|
||||
'Choose Precision, Normal, or Turbo above the drive pad.',
|
||||
'Use the other column for rover controls.',
|
||||
'Rover controls dim while another person has the turn.',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'Docking the rover',
|
||||
items: [
|
||||
'If you see "Docking in Progress", the rover is currently auto-seeking the dock.',
|
||||
{ segments: ['For manual docking, press "Enter Docking Assist" (or "Dock" button).'] },
|
||||
'Assist mode tilts camera down and limits speed for precise alignment.',
|
||||
'Tap "Dock and charge" when finished.',
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
title: 'More controls',
|
||||
items: [
|
||||
'Chat, Activities, VIP, Room Controls, Help, and Settings are below the rover controls.',
|
||||
],
|
||||
},
|
||||
],
|
||||
// aside: [
|
||||
// {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { SettingsProvider } from './settings/index.js'
|
||||
import DeterrenceChaos from './components/DeterrenceChaos/index.jsx'
|
||||
import AnalyticsReporter from './analytics/AnalyticsReporter.jsx'
|
||||
import PtzAppRoot from './ptz/PtzAppRoot.jsx'
|
||||
import InitialSessionOverlay from './components/InitialSessionOverlay/index.jsx'
|
||||
|
||||
// The reporting route includes the charting and CSV libraries. Loading that
|
||||
// bundle only when `/reports` is visited keeps ordinary rover-control sessions
|
||||
@@ -31,6 +32,11 @@ createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<SocketProvider>
|
||||
<SessionProvider>
|
||||
{/* Every route depends on the first authoritative session snapshot.
|
||||
Mounting this opaque layer at the shared provider boundary prevents
|
||||
incomplete route-specific placeholders from flashing while still
|
||||
allowing every application tree to initialize underneath it. */}
|
||||
<InitialSessionOverlay />
|
||||
<TelemetryProvider>
|
||||
<SettingsProvider>
|
||||
<ChatProvider>
|
||||
|
||||
Reference in New Issue
Block a user