idle autodock, alerts on spectator and mini pages

This commit is contained in:
legop3
2026-01-09 15:06:44 -05:00
parent 50d3977814
commit 29154c56e6
6 changed files with 163 additions and 7 deletions
+19
View File
@@ -101,6 +101,15 @@ function formatOiEmoji(oiMode) {
return '❔';
}
function formatDuration(ms) {
if (ms == null) return 'n/a';
const seconds = Math.max(0, Math.round(ms / 1000));
const minutes = Math.floor(seconds / 60);
const remainder = seconds % 60;
if (minutes <= 0) return `${seconds}s`;
return remainder > 0 ? `${minutes}m ${remainder}s` : `${minutes}m`;
}
function isCharging(sensors) {
const label = sensors?.chargingState?.label?.toLowerCase();
const chargingByLabel =
@@ -694,6 +703,16 @@ function handleBusEvent(event) {
});
updatePresence();
break;
case 'rover.dockGuard':
announce({
channelId: channels.adminAlerts,
color: 0xf0b651,
title: 'Dock Guard Triggered',
description: `${payload?.roverId} (${payload?.reasonText || 'undocked'}) for ${formatDuration(
payload?.idleMs,
)}. Seek dock + sensor stream reissued until movement.`,
});
break;
case 'battery.warn':
announce({
channelId: channels.adminAlerts,
+131
View File
@@ -15,9 +15,14 @@ const spectatorSockets = new Set();
const turnService = require('./turnService');
const managerEvents = new EventEmitter();
const DOCK_GUARD_WINDOW_MS = 2 * 1000;
const IDLE_UNDOCKED_MS = 2 * 60 * 1000;
const PASSIVE_UNDOCKED_MS = 60 * 1000;
const DOCK_GUARD_RETRY_MS = 10 * 1000;
const DOCK_COMMAND_BASE64 = Buffer.from([143]).toString('base64');
const BACKOFF_MS = 500;
const BACKOFF_SPEED = 300;
const backoffTimers = new Map(); // roverId -> Timeout
const dockGuardStates = new Map(); // roverId -> guard state
function ensureRecord(id) {
if (!rovers.has(id)) {
@@ -34,6 +39,7 @@ function ensureRecord(id) {
batteryState: null,
room: `rover:${id}`,
lastSeen: Date.now(),
lastMovementAt: Date.now(),
});
}
return rovers.get(id);
@@ -68,6 +74,7 @@ function removeRover(id) {
const record = rovers.get(id);
if (!record) return;
rovers.delete(id);
stopDockGuard(id);
turnService.cleanupRover(id);
spectatorSockets.forEach((socketId) => {
const sock = io.sockets.sockets.get(socketId);
@@ -178,6 +185,7 @@ function handleSensorFrame(roverId, frame) {
const decoded = parseSensorFrame(frame.data);
record.lastSensor = { raw: frame, decoded };
record.batteryState = computeBatteryState(record, decoded);
updateMovement(record, decoded);
const hasDockInfo = decoded?.chargingSources != null;
if (hasDockInfo) {
const prevDocked = record.docked;
@@ -197,8 +205,131 @@ function handleSensorFrame(roverId, frame) {
sensors: decoded,
});
managerEvents.emit('sensor', { roverId, sensors: decoded, batteryState: record.batteryState });
evaluateDockGuard(record, decoded);
}
function updateMovement(record, sensors) {
if (!record || !sensors) return;
const distance = Math.abs(sensors.distanceMm ?? 0);
const angle = Math.abs(sensors.angleDeg ?? 0);
const requested = Math.abs(sensors.requestedVelocity ?? 0);
const requestedLeft = Math.abs(sensors.requestedLeftVelocity ?? 0);
const requestedRight = Math.abs(sensors.requestedRightVelocity ?? 0);
const moving = distance > 0 || angle > 0 || requested > 0 || requestedLeft > 0 || requestedRight > 0;
if (moving) {
record.lastMovementAt = Date.now();
}
}
function getDockGuardState(roverId) {
if (!dockGuardStates.has(roverId)) {
dockGuardStates.set(roverId, {
idleUndockedSince: null,
passiveUndockedSince: null,
active: false,
reason: null,
startedAt: null,
timer: null,
});
}
return dockGuardStates.get(roverId);
}
function evaluateDockGuard(record, sensors) {
if (!record || !sensors) return;
const state = getDockGuardState(record.id);
const now = Date.now();
const docked = Boolean(sensors?.chargingSources?.homeBase);
const oiMode = sensors?.oiMode?.label || null;
const idleMs = record.lastMovementAt ? now - record.lastMovementAt : 0;
const isIdle = idleMs >= 1000;
if (docked || record.drivers.size > 0 || !isIdle) {
state.idleUndockedSince = null;
} else if (!state.idleUndockedSince) {
state.idleUndockedSince = now;
}
if (docked || oiMode !== 'passive' || !isIdle) {
state.passiveUndockedSince = null;
} else if (!state.passiveUndockedSince) {
state.passiveUndockedSince = now;
}
if (state.active) {
const shouldStop =
docked ||
!isIdle ||
(state.reason === 'idle' && record.drivers.size > 0) ||
(state.reason === 'passive' && oiMode !== 'passive');
if (shouldStop) {
stopDockGuard(record.id);
}
return;
}
const idleReady =
state.idleUndockedSince && now - state.idleUndockedSince >= IDLE_UNDOCKED_MS;
const passiveReady =
state.passiveUndockedSince && now - state.passiveUndockedSince >= PASSIVE_UNDOCKED_MS;
if (passiveReady) {
startDockGuard(record, 'passive', idleMs);
} else if (idleReady) {
startDockGuard(record, 'idle', idleMs);
}
}
function startDockGuard(record, reason, idleMs) {
if (!record) return;
const state = getDockGuardState(record.id);
if (state.active) return;
state.active = true;
state.reason = reason;
state.startedAt = Date.now();
const reasonText = reason === 'passive' ? 'passive mode' : 'idle and undocked';
publishEvent({
source: 'roverManager',
type: 'rover.dockGuard',
payload: {
roverId: record.id,
reason,
reasonText,
idleMs,
},
});
attemptDockGuard(record.id);
state.timer = setInterval(() => attemptDockGuard(record.id), DOCK_GUARD_RETRY_MS);
}
function stopDockGuard(roverId) {
const state = dockGuardStates.get(roverId);
if (!state) return;
if (state.timer) {
clearInterval(state.timer);
}
state.active = false;
state.reason = null;
state.startedAt = null;
state.timer = null;
state.idleUndockedSince = null;
state.passiveUndockedSince = null;
}
function attemptDockGuard(roverId) {
const record = rovers.get(roverId);
if (!record) {
stopDockGuard(roverId);
return;
}
const { issueCommand } = require('./commandService');
try {
issueCommand(roverId, { type: 'sensorStream', sensorStream: { enable: true } });
issueCommand(roverId, { type: 'raw', raw: DOCK_COMMAND_BASE64 });
} catch (err) {
logger.warn('Dock guard command failed', roverId, err.message);
}
}
function handleIdleUndock(undockedRecord) {
if (!undockedRecord || undockedRecord.drivers.size > 0) return;
const now = Date.now();