update idleservice so that its based on users not drivers

This commit is contained in:
legop3
2026-07-11 19:06:20 -04:00
parent 924a3c3d55
commit db44d23947
2 changed files with 63 additions and 23 deletions
+62 -22
View File
@@ -1,33 +1,63 @@
// Idle Service // Idle Service
// Purpose: Triggers a modular idle action pipeline after a sustained no-driver period. // Purpose: Triggers a modular idle action pipeline after a sustained no-operator-online period.
// Scope: Observes driver activity events and coordinates timer-based idle automation execution. // Scope: Observes user/admin socket presence and coordinates timer-based idle automation execution.
const logger = require('../../globals/logger').child('idleService'); const logger = require('../../globals/logger').child('idleService');
const { getActiveDrivers, turnEvents } = require('../turnService'); const io = require('../../globals/io');
const roverManager = require('../roverManager'); const { getRole, roleEvents } = require('../roleService');
const { getRecentDriveActivity } = require('../commandService');
const { IDLE_TIMEOUT_MS } = require('./constants'); const { IDLE_TIMEOUT_MS } = require('./constants');
const { runtime } = require('./state'); const { runtime } = require('./state');
const { runIdleActions } = require('./actions'); const { runIdleActions } = require('./actions');
function getActivitySnapshot() { function getActivitySnapshot() {
const active = getActiveDrivers(); let onlineUsers = 0;
const turnCount = active && typeof active === 'object' ? Object.keys(active).length : 0; let onlineAdmins = 0;
const activeByTurn = turnCount; let onlineSpectators = 0;
let onlineIgnored = 0;
let liveCount = 0; io.sockets.sockets.forEach((socket) => {
roverManager.rovers.forEach((record) => { const role = getRole(socket);
if (record?.drivers?.size > 0) liveCount += 1;
/*
Idle automation is about whether a real operator is present, not whether
a browser tab is merely watching. Spectators can leave the room lights,
PTZ emitters, and rovers in their automated idle state because they are
intentionally read-only and cannot be the person still using the setup.
*/
if (role === 'spectator') {
onlineSpectators += 1;
return;
}
/*
Lockdown admins are counted with regular admins because both represent a
person with operator-level access who may be supervising the room without
actively driving a rover. Plain users also count even before they request
control, which is the behavior this service now needs.
*/
if (role === 'admin' || role === 'lockdown') {
onlineAdmins += 1;
return;
}
if (role === 'user') {
onlineUsers += 1;
return;
}
/*
Unknown future roles should not accidentally keep automation disabled.
If a new role should count as an operator, it should be added explicitly
above so this policy remains easy to audit.
*/
onlineIgnored += 1;
}); });
const activeByRoverDrivers = liveCount;
const recentDriveEvents = getRecentDriveActivity(IDLE_TIMEOUT_MS, { excludeAdmins: false }); const totalActive = onlineUsers + onlineAdmins;
const activeByRecentDrive = recentDriveEvents.length;
const totalActive = Math.max(activeByTurn, activeByRoverDrivers, activeByRecentDrive);
return { return {
activeByTurn, onlineUsers,
activeByRoverDrivers, onlineAdmins,
activeByRecentDrive, onlineSpectators,
onlineIgnored,
totalActive, totalActive,
}; };
} }
@@ -53,7 +83,7 @@ function scheduleIdleTimer() {
runtime.deadlineAt = null; runtime.deadlineAt = null;
const activity = getActivitySnapshot(); const activity = getActivitySnapshot();
if (activity.totalActive > 0) { if (activity.totalActive > 0) {
logger.info('Idle automation skipped; active control detected', activity); logger.info('Idle automation skipped; user or admin online', activity);
return; return;
} }
runtime.lastTriggeredAt = Date.now(); runtime.lastTriggeredAt = Date.now();
@@ -77,8 +107,18 @@ function refreshIdleState() {
scheduleIdleTimer(); scheduleIdleTimer();
} }
turnEvents.on('activeDriver', refreshIdleState); io.on('connection', (socket) => {
turnEvents.on('queue', refreshIdleState); /*
A user/admin can be online without ever touching rover controls, so socket
presence has to be a first-class idle signal. The disconnect hook is just as
important: it is what starts the idle timeout after the last non-spectator
leaves, even if no driving event happens around that departure.
*/
refreshIdleState();
socket.on('disconnect', refreshIdleState);
});
roleEvents.on('change', refreshIdleState);
refreshIdleState(); refreshIdleState();
@@ -347,7 +347,7 @@ function startPublisher() {
'-tune', '-tune',
'zerolatency', 'zerolatency',
'-threads', '-threads',
'2', '8',
'-x264-params', '-x264-params',
'sliced-threads=1:sync-lookahead=0:rc-lookahead=0:keyint=20:min-keyint=20:scenecut=0', 'sliced-threads=1:sync-lookahead=0:rc-lookahead=0:keyint=20:min-keyint=20:scenecut=0',
'-bf', '-bf',