mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
chatinpanel
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
File diff suppressed because one or more lines are too long
@@ -78,8 +78,8 @@
|
|||||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DPoEKYWc.js"></script>
|
<script type="module" crossorigin src="/assets/index-BjdaAW59.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-MAURNhur.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dyg2cJnP.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const PTZ_STREAM_PATH = 'ptz-camera';
|
|||||||
const DEFAULT_ONVIF_PORT = 8000;
|
const DEFAULT_ONVIF_PORT = 8000;
|
||||||
const DEFAULT_PROFILE_TOKEN = '003';
|
const DEFAULT_PROFILE_TOKEN = '003';
|
||||||
const DEFAULT_TURN_DURATION_MS = 5 * 60 * 1000;
|
const DEFAULT_TURN_DURATION_MS = 5 * 60 * 1000;
|
||||||
const DOCK_GRACE_MS = 60 * 1000;
|
|
||||||
// PTZ is a normal replay source now, so capture should be on unless the feature
|
// PTZ is a normal replay source now, so capture should be on unless the feature
|
||||||
// explicitly disables replay for the camera.
|
// explicitly disables replay for the camera.
|
||||||
const DEFAULT_REPLAY_ENABLED = true;
|
const DEFAULT_REPLAY_ENABLED = true;
|
||||||
@@ -72,7 +71,6 @@ let onvifCam = null;
|
|||||||
let reolinkClient = null;
|
let reolinkClient = null;
|
||||||
let reolinkModulePromise = null;
|
let reolinkModulePromise = null;
|
||||||
let turnTimer = null;
|
let turnTimer = null;
|
||||||
let blockedTimer = null;
|
|
||||||
let publisherProcess = null;
|
let publisherProcess = null;
|
||||||
let publisherRestartTimer = null;
|
let publisherRestartTimer = null;
|
||||||
let publisherStderrSyncTimer = null;
|
let publisherStderrSyncTimer = null;
|
||||||
@@ -594,15 +592,25 @@ function clearTurnTimer() {
|
|||||||
turnTimer = null;
|
turnTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearBlockedTimer() {
|
|
||||||
if (blockedTimer) clearTimeout(blockedTimer);
|
|
||||||
blockedTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFromQueue(socketId) {
|
function removeFromQueue(socketId) {
|
||||||
state.queue = state.queue.filter((id) => id !== socketId);
|
state.queue = state.queue.filter((id) => id !== socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDockRequiredPayload(socket, leave) {
|
||||||
|
/*
|
||||||
|
PTZ must not become an escape hatch for abandoning the last undocked rover.
|
||||||
|
Keep the payload shape shared between immediate claim rejection and stale
|
||||||
|
queue cleanup so the browser gets one consistent dock-required event.
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
socketId: socket.id,
|
||||||
|
label: getSocketLabel(socket.id),
|
||||||
|
roverId: leave.currentId || null,
|
||||||
|
message: leave.message,
|
||||||
|
until: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function revokeOperator(reason = 'release') {
|
function revokeOperator(reason = 'release') {
|
||||||
if (!state.operatorSocketId) return;
|
if (!state.operatorSocketId) return;
|
||||||
const previous = state.operatorSocketId;
|
const previous = state.operatorSocketId;
|
||||||
@@ -652,8 +660,6 @@ function handleTurnDeadline() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function advanceQueue(reason = 'advance') {
|
function advanceQueue(reason = 'advance') {
|
||||||
clearBlockedTimer();
|
|
||||||
state.blocked = null;
|
|
||||||
if (state.operatorSocketId || !state.queue.length) {
|
if (state.operatorSocketId || !state.queue.length) {
|
||||||
emitChange(reason);
|
emitChange(reason);
|
||||||
return;
|
return;
|
||||||
@@ -668,26 +674,15 @@ function advanceQueue(reason = 'advance') {
|
|||||||
const leave = roverManager.canLeaveCurrentRover(socket);
|
const leave = roverManager.canLeaveCurrentRover(socket);
|
||||||
if (!leave.ok) {
|
if (!leave.ok) {
|
||||||
/*
|
/*
|
||||||
A queued user may reach the camera while still being the last person on an
|
claim() blocks this before queue entry, but this defensive check handles
|
||||||
undocked rover. Hold their queue slot briefly so they can dock; if they do
|
stale state: a user can dock, join the queue, then undock again before
|
||||||
not satisfy the shared rover-leave rule, rotate them to the back and let
|
their PTZ turn arrives. In that case they are removed instead of holding a
|
||||||
the next person try.
|
PTZ queue slot while still responsible for an undocked rover.
|
||||||
*/
|
*/
|
||||||
state.blocked = {
|
removeFromQueue(socket.id);
|
||||||
socketId: socket.id,
|
socket.emit('ptzCamera:dockRequired', buildDockRequiredPayload(socket, leave));
|
||||||
label: getSocketLabel(socket.id),
|
emitChange('drop-dock-required');
|
||||||
roverId: leave.currentId || null,
|
advanceQueue('drop-dock-required');
|
||||||
message: leave.message,
|
|
||||||
until: Date.now() + DOCK_GRACE_MS,
|
|
||||||
};
|
|
||||||
socket.emit('ptzCamera:dockRequired', state.blocked);
|
|
||||||
blockedTimer = setTimeout(() => {
|
|
||||||
const [blockedId] = state.queue.splice(0, 1);
|
|
||||||
if (blockedId) state.queue.push(blockedId);
|
|
||||||
state.blocked = null;
|
|
||||||
advanceQueue('dock-grace-expired');
|
|
||||||
}, DOCK_GRACE_MS);
|
|
||||||
emitChange('dock-required');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activateOperator(socket);
|
activateOperator(socket);
|
||||||
@@ -698,6 +693,12 @@ async function claim(socket) {
|
|||||||
await initialize();
|
await initialize();
|
||||||
if (!state.initialized) throw new Error(state.error || 'PTZ camera is not ready');
|
if (!state.initialized) throw new Error(state.error || 'PTZ camera is not ready');
|
||||||
if (state.operatorSocketId === socket.id) return getPublicState(socket);
|
if (state.operatorSocketId === socket.id) return getPublicState(socket);
|
||||||
|
const leave = roverManager.canLeaveCurrentRover(socket);
|
||||||
|
if (!leave.ok) {
|
||||||
|
const payload = buildDockRequiredPayload(socket, leave);
|
||||||
|
socket.emit('ptzCamera:dockRequired', payload);
|
||||||
|
throw new Error(leave.message);
|
||||||
|
}
|
||||||
if (state.operatorSocketId) {
|
if (state.operatorSocketId) {
|
||||||
if (!state.queue.includes(socket.id)) state.queue.push(socket.id);
|
if (!state.queue.includes(socket.id)) state.queue.push(socket.id);
|
||||||
emitChange('queue-join');
|
emitChange('queue-join');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
|
// Scope: Owns PTZ UI state only; server-side PTZ ownership, rover handoff, and command authorization remain authoritative.
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import ChatPanel from '../ChatPanel/index.jsx';
|
||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
||||||
import ControlPadPanel from '../MobileControls/ControlPadPanel.jsx';
|
import ControlPadPanel from '../MobileControls/ControlPadPanel.jsx';
|
||||||
@@ -358,6 +359,16 @@ function PtzController({ open, onClose, layout = 'desktop' }) {
|
|||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<PtzControlReference />
|
<PtzControlReference />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="min-h-[12rem] flex-1">
|
||||||
|
{/*
|
||||||
|
The global chat key focuses the input registered by ChatPanel through
|
||||||
|
ChatContext. Keeping a real ChatPanel mounted inside the PTZ fullscreen
|
||||||
|
sidebar lets the normal keyboard path focus chat and lets Enter submit
|
||||||
|
through the existing chat composer form instead of adding PTZ-specific
|
||||||
|
chat handling.
|
||||||
|
*/}
|
||||||
|
<ChatPanel fillHeight title="Chat" />
|
||||||
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<ReplaySourcesPanel panelId="ptz-controller-replay" />
|
<ReplaySourcesPanel panelId="ptz-controller-replay" />
|
||||||
</div>
|
</div>
|
||||||
@@ -377,6 +388,13 @@ function PtzController({ open, onClose, layout = 'desktop' }) {
|
|||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<PtzMobileControlsPanel ptz={ptz} disabled={!isOperator} />
|
<PtzMobileControlsPanel ptz={ptz} disabled={!isOperator} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="min-h-[10rem] flex-1">
|
||||||
|
{/*
|
||||||
|
Mobile uses the same ChatPanel registration as desktop so the mapped
|
||||||
|
chat key and the on-screen input stay on one shared chat implementation.
|
||||||
|
*/}
|
||||||
|
<ChatPanel fillHeight title="Chat" />
|
||||||
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<ReplaySourcesPanel panelId="ptz-controller-replay-mobile" />
|
<ReplaySourcesPanel panelId="ptz-controller-replay-mobile" />
|
||||||
</div>
|
</div>
|
||||||
@@ -458,11 +476,12 @@ export default function VipPtzCameraCard({ onMessage, fullWidth = false, layout
|
|||||||
try {
|
try {
|
||||||
const response = await ptzClaim();
|
const response = await ptzClaim();
|
||||||
/*
|
/*
|
||||||
Requesting the PTZ camera is also the user's intent to enter camera mode.
|
Requesting the PTZ camera is only enough to open fullscreen after the
|
||||||
Open the controller immediately for operators and queued users so the
|
server confirms the user actually became the operator or entered the
|
||||||
camera surface does not require a second, redundant "open" click.
|
queue. Dock-required failures reject before queue entry, so they must
|
||||||
|
leave the user on this card with the dock/charge message visible.
|
||||||
*/
|
*/
|
||||||
setControllerOpen(true);
|
if (response?.state?.isOperator || response?.state?.queuedPosition) setControllerOpen(true);
|
||||||
if (response?.state?.isOperator) onMessage?.('PTZ camera turn active.');
|
if (response?.state?.isOperator) onMessage?.('PTZ camera turn active.');
|
||||||
else if (response?.state?.queuedPosition) onMessage?.(`Joined PTZ queue at position ${response.state.queuedPosition}.`);
|
else if (response?.state?.queuedPosition) onMessage?.(`Joined PTZ queue at position ${response.state.queuedPosition}.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user