mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
279 lines
8.6 KiB
JavaScript
279 lines
8.6 KiB
JavaScript
const EventEmitter = require('events');
|
|
const io = require('../globals/io');
|
|
const logger = require('../globals/logger').child('assignment');
|
|
const { MODES, getMode, modeEvents } = require('./modeManager');
|
|
const { roleEvents, getRole, isAdmin, isLockdownAdmin } = require('./roleService');
|
|
const roverManager = require('./roverManager');
|
|
|
|
const socketRefs = new Map(); // socketId -> socket
|
|
const assignments = new Map(); // socketId -> roverId
|
|
const waiting = new Set(); // socketIds waiting for placement
|
|
const assignmentEvents = new EventEmitter();
|
|
|
|
io.on('connection', (socket) => {
|
|
socketRefs.set(socket.id, socket);
|
|
socket.on('disconnect', () => {
|
|
socketRefs.delete(socket.id);
|
|
unassignSocket(socket);
|
|
});
|
|
});
|
|
|
|
roleEvents.on('change', ({ socket, role }) => {
|
|
if (!socket || !socket.id) return;
|
|
if (role === 'user') {
|
|
assignSocket(socket);
|
|
} else {
|
|
unassignSocket(socket);
|
|
}
|
|
});
|
|
|
|
modeEvents.on('change', (mode) => {
|
|
if (mode === MODES.ADMIN) {
|
|
for (const [socketId, roverId] of assignments.entries()) {
|
|
const socket = socketRefs.get(socketId);
|
|
if (socket && !isAdmin(socket)) {
|
|
releaseAssignment(socket, roverId);
|
|
}
|
|
}
|
|
} else if (mode === MODES.LOCKDOWN) {
|
|
for (const [socketId, roverId] of assignments.entries()) {
|
|
const socket = socketRefs.get(socketId);
|
|
if (socket && !isLockdownAdmin(socket)) {
|
|
releaseAssignment(socket, roverId);
|
|
}
|
|
}
|
|
}
|
|
reassignWaiting();
|
|
});
|
|
|
|
roverManager.managerEvents.on('lock', ({ roverId, locked }) => {
|
|
if (locked) {
|
|
reassignFromRover(roverId);
|
|
} else {
|
|
reassignWaiting();
|
|
}
|
|
});
|
|
|
|
roverManager.managerEvents.on('private', ({ roverId, open }) => {
|
|
if (open) {
|
|
reassignWaiting();
|
|
} else {
|
|
reassignFromRover(roverId);
|
|
}
|
|
});
|
|
|
|
roverManager.managerEvents.on('rover', ({ action }) => {
|
|
if (action === 'removed' || action === 'upsert') {
|
|
reassignWaiting();
|
|
}
|
|
});
|
|
|
|
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, options = {}) {
|
|
if (!socket || isAdmin(socket) || getRole(socket) !== 'user') {
|
|
return;
|
|
}
|
|
// avoid double assignment
|
|
if (assignments.has(socket.id)) {
|
|
return;
|
|
}
|
|
const target = pickRover(socket, {
|
|
excludeRoverId: options.excludeRoverId || null,
|
|
});
|
|
if (!target) {
|
|
waiting.add(socket.id);
|
|
logger.info('No rover available, user waiting', socket.id);
|
|
assignmentEvents.emit('update', socket.id);
|
|
return;
|
|
}
|
|
try {
|
|
roverManager.requestControl(target.id, socket, { allowUser: true });
|
|
assignments.set(socket.id, target.id);
|
|
waiting.delete(socket.id);
|
|
logger.info('Assigned user to rover', socket.id, target.id);
|
|
assignmentEvents.emit('update', socket.id);
|
|
} catch (err) {
|
|
logger.warn('Failed to assign user', err.message);
|
|
waiting.add(socket.id);
|
|
assignmentEvents.emit('update', socket.id);
|
|
}
|
|
}
|
|
|
|
function unassignSocket(socket) {
|
|
if (!socket) return;
|
|
waiting.delete(socket.id);
|
|
const roverId = assignments.get(socket.id);
|
|
if (roverId) {
|
|
roverManager.releaseControl(roverId, socket);
|
|
assignments.delete(socket.id);
|
|
logger.info('Unassigned socket from rover', socket.id, roverId);
|
|
assignmentEvents.emit('update', socket.id);
|
|
}
|
|
}
|
|
|
|
function reassignFromRover(roverId) {
|
|
for (const [socketId, rid] of assignments.entries()) {
|
|
if (rid !== roverId) continue;
|
|
const socket = socketRefs.get(socketId);
|
|
if (!socket) {
|
|
assignments.delete(socketId);
|
|
continue;
|
|
}
|
|
const access = roverManager.canRequestControl(roverId, socket, { allowUser: true });
|
|
if (access.ok) {
|
|
continue;
|
|
}
|
|
roverManager.releaseControl(rid, socket);
|
|
assignments.delete(socketId);
|
|
assignSocket(socket);
|
|
assignmentEvents.emit('update', socketId);
|
|
}
|
|
}
|
|
|
|
function reassignWaiting() {
|
|
for (const socketId of Array.from(waiting)) {
|
|
const socket = socketRefs.get(socketId);
|
|
if (socket) {
|
|
assignSocket(socket);
|
|
} else {
|
|
waiting.delete(socketId);
|
|
}
|
|
assignmentEvents.emit('update', socketId);
|
|
}
|
|
}
|
|
|
|
function releaseAssignment(socket, roverId) {
|
|
roverManager.releaseControl(roverId, socket);
|
|
assignments.delete(socket.id);
|
|
waiting.add(socket.id);
|
|
logger.info('Released assignment back to queue', socket.id, roverId);
|
|
assignmentEvents.emit('update', socket.id);
|
|
}
|
|
|
|
function forceRelease(roverId, socketId) {
|
|
const socket = socketRefs.get(socketId) || io.sockets.sockets.get(socketId);
|
|
if (assignments.get(socketId) === roverId) {
|
|
assignments.delete(socketId);
|
|
}
|
|
waiting.delete(socketId);
|
|
if (socket) {
|
|
roverManager.releaseControl(roverId, socket);
|
|
logger.info('Force released socket from rover', socketId, roverId);
|
|
} else {
|
|
logger.warn('Force release: socket not found', socketId, roverId);
|
|
}
|
|
assignmentEvents.emit('update', socketId);
|
|
}
|
|
|
|
function pickRover(socket, options = {}) {
|
|
const mode = getMode();
|
|
if (mode === MODES.ADMIN || mode === MODES.LOCKDOWN) {
|
|
return null;
|
|
}
|
|
const allCandidates = Array.from(roverManager.rovers.values()).filter((rover) => {
|
|
if (!rover || rover.locked) return false;
|
|
const access = roverManager.canRequestControl(rover.id, socket, { allowUser: true });
|
|
if (!access.ok) return false;
|
|
return true;
|
|
});
|
|
let candidates = allCandidates;
|
|
const excludeRoverId = options?.excludeRoverId || null;
|
|
if (excludeRoverId) {
|
|
const withoutExcluded = allCandidates.filter((rover) => String(rover.id) !== String(excludeRoverId));
|
|
if (withoutExcluded.length > 0) {
|
|
candidates = withoutExcluded;
|
|
}
|
|
}
|
|
if (candidates.length === 0) {
|
|
return null;
|
|
}
|
|
const dockedRank = (rover) => {
|
|
if (!rover) return 0;
|
|
if (rover.docked === true) return -1;
|
|
if (rover.docked === false) return 1;
|
|
const sensors = rover.lastSensor?.decoded || rover.lastSensor?.sensors || null;
|
|
const docked = sensors?.chargingSources?.homeBase;
|
|
if (docked === true) return -1;
|
|
if (docked === false) return 1;
|
|
return 0;
|
|
};
|
|
const idleRank = (rover) => (rover?.drivers?.size === 0 ? 1 : 0);
|
|
const compare = (a, b) => {
|
|
const aEmpty = idleRank(a);
|
|
const bEmpty = idleRank(b);
|
|
if (aEmpty !== bEmpty) return bEmpty - aEmpty;
|
|
const aDockRank = dockedRank(a);
|
|
const bDockRank = dockedRank(b);
|
|
if (aEmpty === 1 && aDockRank !== bDockRank) {
|
|
return bDockRank - aDockRank;
|
|
}
|
|
if (a.drivers.size !== b.drivers.size) {
|
|
return a.drivers.size - b.drivers.size;
|
|
}
|
|
return bDockRank - aDockRank;
|
|
};
|
|
candidates.sort(compare);
|
|
const best = candidates[0];
|
|
if (!best) return null;
|
|
const bestTier = candidates.filter((entry) => compare(entry, best) === 0);
|
|
if (!bestTier.length) return best;
|
|
return bestTier[Math.floor(Math.random() * bestTier.length)] || best;
|
|
}
|
|
|
|
function rerollAssignments() {
|
|
const users = Array.from(socketRefs.values()).filter((socket) => socket && getRole(socket) === 'user');
|
|
const previous = new Map(users.map((socket) => [socket.id, assignments.get(socket.id) || null]));
|
|
users.forEach((socket) => {
|
|
unassignSocket(socket);
|
|
});
|
|
let moved = 0;
|
|
users.forEach((socket) => {
|
|
const prevRover = previous.get(socket.id) || null;
|
|
assignSocket(socket, { excludeRoverId: prevRover });
|
|
const nextRover = assignments.get(socket.id) || null;
|
|
if (nextRover && prevRover && String(nextRover) !== String(prevRover)) {
|
|
moved += 1;
|
|
}
|
|
});
|
|
return moved;
|
|
}
|
|
|
|
function describeAssignment(socketId) {
|
|
const assignedRoverId = assignments.get(socketId) || null;
|
|
const adminRoverId = assignedRoverId ? null : roverManager.getPrimaryRoverForSocket(socketId);
|
|
const waitingIndex = waiting.has(socketId) ? Array.from(waiting).indexOf(socketId) : -1;
|
|
const roverId = assignedRoverId || adminRoverId || null;
|
|
const waitingStatus = waiting.has(socketId);
|
|
return {
|
|
roverId,
|
|
status: assignedRoverId ? 'assigned' : adminRoverId ? 'admin' : waitingStatus ? 'waiting' : null,
|
|
queuePosition: waitingIndex >= 0 ? waitingIndex + 1 : null,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
assignmentEvents,
|
|
describeAssignment,
|
|
forceRelease,
|
|
rerollAssignments,
|
|
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);
|
|
},
|
|
};
|