mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
194 lines
6.0 KiB
JavaScript
194 lines
6.0 KiB
JavaScript
// rover Manager socket handlers
|
|
// Purpose: Encapsulates rover-manager socket event wiring while preserving existing control/lock/spectator behavior.
|
|
// Scope: Keeps runtime behavior unchanged by delegating all side effects to injected rover-manager callbacks.
|
|
function registerSocketHandlers(deps) {
|
|
const {
|
|
io,
|
|
logger,
|
|
MODES,
|
|
ALERT_COLOR,
|
|
getMode,
|
|
isAdmin,
|
|
isLockdownAdmin,
|
|
sendAlert,
|
|
videoSessions,
|
|
rovers,
|
|
isPrivateRecord,
|
|
isRoverVisibleToSocket,
|
|
tickPrivateAutoClose,
|
|
removeSocket,
|
|
enableSpectator,
|
|
canRequestControl,
|
|
canSwitchRover,
|
|
getRoversForSocket,
|
|
requestControl,
|
|
releaseControl,
|
|
managerEvents,
|
|
setPrivateOpen,
|
|
lockRover,
|
|
setPrivateSafety,
|
|
} = deps;
|
|
|
|
io.on('connection', (socket) => {
|
|
tickPrivateAutoClose();
|
|
if (socket.data?.role === 'spectator') {
|
|
enableSpectator(socket);
|
|
}
|
|
|
|
function handleRequestControl({ roverId, force } = {}, cb = () => {}) {
|
|
try {
|
|
if (socket.data?.role === 'spectator') {
|
|
throw new Error('Spectators cannot drive');
|
|
}
|
|
const mode = getMode();
|
|
if (
|
|
(mode === MODES.ADMIN && !isAdmin(socket)) ||
|
|
(mode === MODES.LOCKDOWN && !isLockdownAdmin(socket))
|
|
) {
|
|
throw new Error('Admins only');
|
|
}
|
|
const fallbackTargetId = Array.from(rovers.keys()).find(
|
|
(id) => canRequestControl(id, socket, { allowUser: true }).ok,
|
|
);
|
|
const targetId = roverId || fallbackTargetId;
|
|
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');
|
|
}
|
|
}
|
|
const forceAllowed = Boolean(force) && isAdmin(socket);
|
|
logger.info('Request control', socket.id, targetId, { force: forceAllowed });
|
|
requestControl(targetId, socket, { force: forceAllowed, 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 });
|
|
cb({ success: true, roverId: targetId });
|
|
} catch (err) {
|
|
logger.warn('Request control failed', socket.id, err.message);
|
|
sendAlert({ color: ALERT_COLOR, title: 'Control denied', message: err.message });
|
|
cb({ error: err.message });
|
|
}
|
|
}
|
|
|
|
function handleReleaseControl({ roverId } = {}, cb = () => {}) {
|
|
if (!roverId) {
|
|
cb({ error: 'roverId required' });
|
|
return;
|
|
}
|
|
logger.info('Release control', socket.id, roverId);
|
|
releaseControl(roverId, socket);
|
|
cb({ success: true, roverId });
|
|
}
|
|
|
|
function handleLockToggle({ roverId, locked } = {}, cb = () => {}) {
|
|
const record = rovers.get(roverId);
|
|
if (!record) {
|
|
cb({ error: 'Unknown rover' });
|
|
return;
|
|
}
|
|
const isPrivate = isPrivateRecord(record);
|
|
if (isPrivate && !isLockdownAdmin(socket)) {
|
|
cb({ error: 'Not authorized' });
|
|
return;
|
|
}
|
|
if (!isPrivate && !isAdmin(socket)) {
|
|
cb({ error: 'Not authorized' });
|
|
return;
|
|
}
|
|
try {
|
|
if (isPrivate) {
|
|
const open = !Boolean(locked);
|
|
setPrivateOpen(roverId, open, { reason: 'manual' });
|
|
logger.info('Private state changed', roverId, { open });
|
|
} else {
|
|
lockRover(roverId, locked, { reason: 'manual' });
|
|
logger.info('Lock state changed', roverId, locked);
|
|
}
|
|
cb({ success: true });
|
|
} catch (err) {
|
|
logger.warn('Lock change failed', roverId, err.message);
|
|
sendAlert({ color: ALERT_COLOR, title: 'Lock failed', message: err.message });
|
|
cb({ error: err.message });
|
|
}
|
|
}
|
|
|
|
function handlePrivateSafetySet({ roverId, safety } = {}, cb = () => {}) {
|
|
const record = rovers.get(roverId);
|
|
if (!record) {
|
|
cb({ error: 'Unknown rover' });
|
|
return;
|
|
}
|
|
if (!isPrivateRecord(record)) {
|
|
cb({ error: 'Rover is not private' });
|
|
return;
|
|
}
|
|
if (!isLockdownAdmin(socket)) {
|
|
cb({ error: 'Not authorized' });
|
|
return;
|
|
}
|
|
try {
|
|
const next = setPrivateSafety(roverId, safety || {}, { reason: 'manual' });
|
|
cb({ success: true, safety: next });
|
|
} catch (err) {
|
|
cb({ error: err.message });
|
|
}
|
|
}
|
|
|
|
function handleSubscribeAll(_, cb = () => {}) {
|
|
if (socket.data?.role !== 'spectator') {
|
|
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()) {
|
|
if (isRoverVisibleToSocket(record, socket)) {
|
|
socket.join(record.room);
|
|
} else {
|
|
socket.leave(record.room);
|
|
}
|
|
}
|
|
cb({ success: true });
|
|
}
|
|
|
|
socket.on('session:requestControl', handleRequestControl);
|
|
socket.on('session:releaseControl', handleReleaseControl);
|
|
socket.on('session:lockRover', handleLockToggle);
|
|
socket.on('session:privateSafety:set', handlePrivateSafetySet);
|
|
socket.on('session:subscribeAll', handleSubscribeAll);
|
|
|
|
socket.on('disconnecting', () => {
|
|
logger.info('Socket disconnecting', socket.id);
|
|
removeSocket(socket);
|
|
tickPrivateAutoClose();
|
|
});
|
|
socket.on('disconnect', () => {
|
|
logger.info('Socket disconnected', socket.id);
|
|
removeSocket(socket);
|
|
tickPrivateAutoClose();
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
registerSocketHandlers,
|
|
};
|