allow everyone to switch rovers conditionally

This commit is contained in:
legop3
2025-12-08 03:50:36 -05:00
parent e9fd5f55a8
commit 161929e280
5 changed files with 84 additions and 8 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BCyA38CU.js"></script>
<script type="module" crossorigin src="/assets/index-6QuSGuDC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DusXB4R9.css">
</head>
<body>
+20
View File
@@ -54,6 +54,15 @@ roverManager.managerEvents.on('rover', ({ action }) => {
}
});
roverManager.managerEvents.on('switch', ({ socketId, roverId }) => {
if (!socketId || !roverId) return;
const socket = socketRefs.get(socketId);
if (!socket) return;
assignments.set(socketId, roverId);
waiting.delete(socketId);
assignmentEvents.emit('update', socketId);
});
function assignSocket(socket) {
if (!socket || isAdmin(socket) || getRole(socket) !== 'user') {
return;
@@ -177,4 +186,15 @@ module.exports = {
assignmentEvents,
describeAssignment,
forceRelease,
getAssignedRover: (socketId) => assignments.get(socketId) || null,
moveAssignment: (socket, roverId, { releasePrevious = true } = {}) => {
if (!socket || !roverId) return;
const previous = assignments.get(socket.id);
if (previous && previous !== roverId && releasePrevious) {
roverManager.releaseControl(previous, socket);
}
assignments.set(socket.id, roverId);
waiting.delete(socket.id);
assignmentEvents.emit('update', socket.id);
},
};
+60 -1
View File
@@ -6,6 +6,7 @@ const { parseSensorFrame } = require('../helpers/sensorDecoder');
const { MODES, getMode } = require('./modeManager');
const { isAdmin, roleEvents } = require('./roleService');
const { publishEvent } = require('./eventBus');
const videoSessions = require('./videoSessions');
const rovers = new Map(); // roverId -> record
const socketToRovers = new Map(); // socketId -> Set(roverId)
@@ -276,6 +277,41 @@ function getPrimaryRoverForSocket(socketId) {
return first.done ? null : first.value;
}
function isDockedAndCharging(record) {
const sensors = record?.lastSensor?.decoded || record?.lastSensor?.sensors || null;
if (!sensors) return false;
const docked = Boolean(sensors.chargingSources?.homeBase);
const code = sensors.chargingState?.code;
const charging = code === 2 || code === 3 || code === 4;
return docked && charging;
}
function hasOtherDrivers(record, socketId) {
if (!record) return false;
for (const driver of record.drivers) {
if (driver !== socketId) return true;
}
return false;
}
function canSwitchRover(socket, targetRoverId) {
const currentId = getPrimaryRoverForSocket(socket.id);
if (!currentId || currentId === targetRoverId) {
return { ok: true, currentId };
}
const currentRecord = rovers.get(currentId);
if (!currentRecord) {
return { ok: true, currentId };
}
if (hasOtherDrivers(currentRecord, socket.id)) {
return { ok: true, currentId };
}
if (isDockedAndCharging(currentRecord)) {
return { ok: true, currentId };
}
return { ok: false, currentId, message: 'Dock and charge your current rover before switching.' };
}
module.exports = {
upsertRover,
removeRover,
@@ -312,12 +348,35 @@ io.on('connection', (socket) => {
function handleRequestControl({ roverId, force } = {}, cb = () => {}) {
try {
if (socket.data?.role === 'spectator') {
throw new Error('Spectators cannot drive');
}
const targetId = roverId || Array.from(rovers.keys())[0];
if (!targetId) {
throw new Error('No rovers available');
}
const previousJoined = getRoversForSocket(socket.id);
if (!isAdmin(socket)) {
const { ok, message } = canSwitchRover(socket, targetId);
if (!ok) {
throw new Error(message || 'Switch denied');
}
}
logger.info('Request control', socket.id, targetId, { force });
requestControl(targetId, socket, { force: Boolean(force) });
requestControl(targetId, socket, { force: Boolean(force), allowUser: true });
previousJoined.forEach((rid) => {
if (rid !== targetId) {
releaseControl(rid, socket);
}
});
videoSessions.revokeWhere(
(info) =>
info.socketId === socket.id &&
info.sourceType === 'rover' &&
info.sourceId !== targetId &&
info.sourceId !== `${targetId}-audio`,
);
managerEvents.emit('switch', { socketId: socket.id, roverId: targetId });
socket.emit('controlGranted', { roverId: targetId });
cb({ success: true, roverId: targetId });
} catch (err) {
+2 -5
View File
@@ -29,10 +29,7 @@ export default function TelemetryPanel() {
const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId);
return user?.nickname || activeDriverId.slice(0, 6);
}, [activeDriverId, roverId, session?.socketId, session?.users]);
const isAdmin = useMemo(
() => session?.role === 'admin' || session?.role === 'lockdown' || session?.role === 'lockdown-admin',
[session?.role],
);
const canRequest = useMemo(() => session?.role && session.role !== 'spectator', [session?.role]);
const [pending, setPending] = useState({});
async function handleRequest(roverId) {
@@ -102,7 +99,7 @@ export default function TelemetryPanel() {
title="Rovers"
roster={roster}
renderActions={(rover) =>
isAdmin ? (
canRequest ? (
<button
type="button"
onClick={() => handleRequest(rover.id)}