mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
idle autodock, alerts on spectator and mini pages
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BvGXjw27.js"></script>
|
<script type="module" crossorigin src="/assets/index-ppyUE0Ej.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-aZHU9GG3.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-aZHU9GG3.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -101,6 +101,15 @@ function formatOiEmoji(oiMode) {
|
|||||||
return '❔';
|
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) {
|
function isCharging(sensors) {
|
||||||
const label = sensors?.chargingState?.label?.toLowerCase();
|
const label = sensors?.chargingState?.label?.toLowerCase();
|
||||||
const chargingByLabel =
|
const chargingByLabel =
|
||||||
@@ -694,6 +703,16 @@ function handleBusEvent(event) {
|
|||||||
});
|
});
|
||||||
updatePresence();
|
updatePresence();
|
||||||
break;
|
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':
|
case 'battery.warn':
|
||||||
announce({
|
announce({
|
||||||
channelId: channels.adminAlerts,
|
channelId: channels.adminAlerts,
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ const spectatorSockets = new Set();
|
|||||||
const turnService = require('./turnService');
|
const turnService = require('./turnService');
|
||||||
const managerEvents = new EventEmitter();
|
const managerEvents = new EventEmitter();
|
||||||
const DOCK_GUARD_WINDOW_MS = 2 * 1000;
|
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_MS = 500;
|
||||||
const BACKOFF_SPEED = 300;
|
const BACKOFF_SPEED = 300;
|
||||||
const backoffTimers = new Map(); // roverId -> Timeout
|
const backoffTimers = new Map(); // roverId -> Timeout
|
||||||
|
const dockGuardStates = new Map(); // roverId -> guard state
|
||||||
|
|
||||||
function ensureRecord(id) {
|
function ensureRecord(id) {
|
||||||
if (!rovers.has(id)) {
|
if (!rovers.has(id)) {
|
||||||
@@ -34,6 +39,7 @@ function ensureRecord(id) {
|
|||||||
batteryState: null,
|
batteryState: null,
|
||||||
room: `rover:${id}`,
|
room: `rover:${id}`,
|
||||||
lastSeen: Date.now(),
|
lastSeen: Date.now(),
|
||||||
|
lastMovementAt: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return rovers.get(id);
|
return rovers.get(id);
|
||||||
@@ -68,6 +74,7 @@ function removeRover(id) {
|
|||||||
const record = rovers.get(id);
|
const record = rovers.get(id);
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
rovers.delete(id);
|
rovers.delete(id);
|
||||||
|
stopDockGuard(id);
|
||||||
turnService.cleanupRover(id);
|
turnService.cleanupRover(id);
|
||||||
spectatorSockets.forEach((socketId) => {
|
spectatorSockets.forEach((socketId) => {
|
||||||
const sock = io.sockets.sockets.get(socketId);
|
const sock = io.sockets.sockets.get(socketId);
|
||||||
@@ -178,6 +185,7 @@ function handleSensorFrame(roverId, frame) {
|
|||||||
const decoded = parseSensorFrame(frame.data);
|
const decoded = parseSensorFrame(frame.data);
|
||||||
record.lastSensor = { raw: frame, decoded };
|
record.lastSensor = { raw: frame, decoded };
|
||||||
record.batteryState = computeBatteryState(record, decoded);
|
record.batteryState = computeBatteryState(record, decoded);
|
||||||
|
updateMovement(record, decoded);
|
||||||
const hasDockInfo = decoded?.chargingSources != null;
|
const hasDockInfo = decoded?.chargingSources != null;
|
||||||
if (hasDockInfo) {
|
if (hasDockInfo) {
|
||||||
const prevDocked = record.docked;
|
const prevDocked = record.docked;
|
||||||
@@ -197,8 +205,131 @@ function handleSensorFrame(roverId, frame) {
|
|||||||
sensors: decoded,
|
sensors: decoded,
|
||||||
});
|
});
|
||||||
managerEvents.emit('sensor', { roverId, sensors: decoded, batteryState: record.batteryState });
|
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) {
|
function handleIdleUndock(undockedRecord) {
|
||||||
if (!undockedRecord || undockedRecord.drivers.size > 0) return;
|
if (!undockedRecord || undockedRecord.drivers.size > 0) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useSpectatorMode } from '../hooks/useSpectatorMode.js';
|
|||||||
import { useRoomCameraSnapshots } from '../hooks/useRoomCameraSnapshots.js';
|
import { useRoomCameraSnapshots } from '../hooks/useRoomCameraSnapshots.js';
|
||||||
import VideoTile from '../components/VideoTile.jsx';
|
import VideoTile from '../components/VideoTile.jsx';
|
||||||
import ChatPanel from '../components/ChatPanel.jsx';
|
import ChatPanel from '../components/ChatPanel.jsx';
|
||||||
|
import AlertFeed from '../components/AlertFeed.jsx';
|
||||||
|
|
||||||
const ROTATE_MS = 20000;
|
const ROTATE_MS = 20000;
|
||||||
|
|
||||||
@@ -148,7 +149,10 @@ function MiniSummaryContent() {
|
|||||||
export default function MiniSummaryApp() {
|
export default function MiniSummaryApp() {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<MiniSummaryContent />
|
<>
|
||||||
|
<MiniSummaryContent />
|
||||||
|
<AlertFeed />
|
||||||
|
</>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import UserListPanel from '../components/UserListPanel.jsx';
|
|||||||
import ChatPanel from '../components/ChatPanel.jsx';
|
import ChatPanel from '../components/ChatPanel.jsx';
|
||||||
import LogPanel from '../components/LogPanel.jsx';
|
import LogPanel from '../components/LogPanel.jsx';
|
||||||
import RoverRoster from '../components/RoverRoster.jsx';
|
import RoverRoster from '../components/RoverRoster.jsx';
|
||||||
|
import AlertFeed from '../components/AlertFeed.jsx';
|
||||||
|
|
||||||
function formatDriverLabel({ roverId, session }) {
|
function formatDriverLabel({ roverId, session }) {
|
||||||
const activeDriverId = session?.activeDrivers?.[roverId] || null;
|
const activeDriverId = session?.activeDrivers?.[roverId] || null;
|
||||||
@@ -138,6 +139,7 @@ export default function SpectatorApp() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<AlertFeed />
|
||||||
</div>
|
</div>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user