mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
178 lines
6.1 KiB
JavaScript
178 lines
6.1 KiB
JavaScript
// Rover Help Monitor
|
|
// Purpose: Converts sustained 600-series Roomba sensor conditions into one help state.
|
|
// Scope: Owns timing and reason state without performing roster fanout, alerts, or Discord I/O.
|
|
const WHEEL_DROP_HELP_MS = 15 * 60 * 1000;
|
|
const CLIFF_HELP_MS = 10 * 60 * 1000;
|
|
const DOCK_GUARD_HELP_MS = 15 * 60 * 1000;
|
|
|
|
const REASON_LABELS = Object.freeze({
|
|
wheelDrop: 'a wheel-drop sensor remained active for 15 minutes',
|
|
cliff: 'the same cliff-sensor pattern remained active for 10 minutes',
|
|
docking: 'automatic docking remained active for 15 minutes',
|
|
});
|
|
|
|
function cliffPattern(sensors) {
|
|
// A four-bit pattern distinguishes one continuously held physical situation
|
|
// from a rover encountering different edges. Zero means no active cliff and
|
|
// therefore cannot begin or retain a cliff-help timer.
|
|
return [
|
|
sensors?.cliffLeft,
|
|
sensors?.cliffFrontLeft,
|
|
sensors?.cliffFrontRight,
|
|
sensors?.cliffRight,
|
|
].reduce((pattern, active, index) => pattern | (active ? 1 << index : 0), 0);
|
|
}
|
|
|
|
function createRoverHelpMonitor({ now = () => Date.now(), onChange = () => {} } = {}) {
|
|
const states = new Map();
|
|
|
|
function ensureState(roverId) {
|
|
const id = String(roverId);
|
|
if (!states.has(id)) {
|
|
states.set(id, {
|
|
wheelDropSince: null,
|
|
cliffPattern: 0,
|
|
cliffPatternSince: null,
|
|
dockGuardSince: null,
|
|
dockGuardSawPassive: false,
|
|
reasons: new Set(),
|
|
});
|
|
}
|
|
return states.get(id);
|
|
}
|
|
|
|
function updateReason(roverId, state, reason, active) {
|
|
const hadReason = state.reasons.has(reason);
|
|
if (active === hadReason) return;
|
|
if (active) state.reasons.add(reason);
|
|
else state.reasons.delete(reason);
|
|
|
|
// Notify on every reason-set change so the integration can update the
|
|
// aggregate flag correctly when one condition clears but another remains.
|
|
onChange({
|
|
roverId: String(roverId),
|
|
needsHelp: state.reasons.size > 0,
|
|
addedReason: active ? reason : null,
|
|
removedReason: active ? null : reason,
|
|
reasons: Array.from(state.reasons),
|
|
});
|
|
}
|
|
|
|
function handleSensor(roverId, sensors) {
|
|
if (!roverId || !sensors) return;
|
|
const state = ensureState(roverId);
|
|
const timestamp = now();
|
|
const docked = Boolean(sensors?.chargingSources?.homeBase);
|
|
|
|
if (docked) {
|
|
// A 600-series Roomba can legitimately rest on the dock with wheel-drop
|
|
// or cliff bits held by its physical position and the nearby surface.
|
|
// Home-base contact is therefore a stronger signal than every monitored
|
|
// fault here: reset all persistence history and do not let time spent
|
|
// docked contribute toward a later HELP after it leaves the base.
|
|
state.wheelDropSince = null;
|
|
state.cliffPattern = 0;
|
|
state.cliffPatternSince = null;
|
|
state.dockGuardSince = null;
|
|
state.dockGuardSawPassive = false;
|
|
updateReason(roverId, state, 'wheelDrop', false);
|
|
updateReason(roverId, state, 'cliff', false);
|
|
updateReason(roverId, state, 'docking', false);
|
|
return;
|
|
}
|
|
|
|
const wheelDrop = Boolean(
|
|
sensors?.bumpsAndWheelDrops?.wheelDropLeft || sensors?.bumpsAndWheelDrops?.wheelDropRight,
|
|
);
|
|
|
|
if (wheelDrop) {
|
|
if (state.wheelDropSince == null) state.wheelDropSince = timestamp;
|
|
} else {
|
|
state.wheelDropSince = null;
|
|
}
|
|
updateReason(
|
|
roverId,
|
|
state,
|
|
'wheelDrop',
|
|
state.wheelDropSince != null && timestamp - state.wheelDropSince >= WHEEL_DROP_HELP_MS,
|
|
);
|
|
|
|
const nextCliffPattern = cliffPattern(sensors);
|
|
if (!nextCliffPattern) {
|
|
state.cliffPattern = 0;
|
|
state.cliffPatternSince = null;
|
|
} else if (nextCliffPattern !== state.cliffPattern) {
|
|
// Any changed combination is new evidence on a non-mapping Roomba, not
|
|
// proof that its chassis translated. Restart only the persistence timer;
|
|
// encoder counts are deliberately not consulted anywhere in this monitor.
|
|
state.cliffPattern = nextCliffPattern;
|
|
state.cliffPatternSince = timestamp;
|
|
}
|
|
updateReason(
|
|
roverId,
|
|
state,
|
|
'cliff',
|
|
state.cliffPatternSince != null && timestamp - state.cliffPatternSince >= CLIFF_HELP_MS,
|
|
);
|
|
|
|
if (state.dockGuardSince != null) {
|
|
const oiMode = sensors?.oiMode?.label || null;
|
|
if (oiMode === 'passive') state.dockGuardSawPassive = true;
|
|
if (state.dockGuardSawPassive && oiMode && oiMode !== 'passive') {
|
|
// Dock guard itself stops as soon as the 600-series wheels begin their
|
|
// autonomous seek motion. Continue timing that seek after the guard
|
|
// interval ends, and clear only on docking or a confirmed exit from the
|
|
// passive OI mode used by opcode 143.
|
|
state.dockGuardSince = null;
|
|
state.dockGuardSawPassive = false;
|
|
}
|
|
}
|
|
|
|
updateReason(
|
|
roverId,
|
|
state,
|
|
'docking',
|
|
state.dockGuardSince != null && timestamp - state.dockGuardSince >= DOCK_GUARD_HELP_MS,
|
|
);
|
|
}
|
|
|
|
function handleDockGuard({ roverId, active, startedAt = null } = {}) {
|
|
if (!roverId) return;
|
|
const state = ensureState(roverId);
|
|
if (active) {
|
|
// Prefer roverManager's authoritative start time. The fallback keeps the
|
|
// monitor deterministic if an event source omits it in a future caller.
|
|
state.dockGuardSince = Number.isFinite(Number(startedAt)) ? Number(startedAt) : now();
|
|
state.dockGuardSawPassive = false;
|
|
return;
|
|
}
|
|
// Before passive mode is observed, a stopped guard means docking never
|
|
// began. Once passive has been seen, wheel activity stops the guard even
|
|
// though the Roomba is still autonomously seeking its dock, so sensor mode
|
|
// and charging state become the authoritative completion signals instead.
|
|
if (!state.dockGuardSawPassive) {
|
|
state.dockGuardSince = null;
|
|
updateReason(roverId, state, 'docking', false);
|
|
}
|
|
}
|
|
|
|
function removeRover(roverId) {
|
|
states.delete(String(roverId));
|
|
}
|
|
|
|
return {
|
|
handleSensor,
|
|
handleDockGuard,
|
|
removeRover,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
CLIFF_HELP_MS,
|
|
DOCK_GUARD_HELP_MS,
|
|
REASON_LABELS,
|
|
WHEEL_DROP_HELP_MS,
|
|
cliffPattern,
|
|
createRoverHelpMonitor,
|
|
};
|