This commit is contained in:
legop3
2026-04-28 19:53:10 -04:00
parent f13b85109e
commit 49f801330f
59 changed files with 686 additions and 401 deletions
+191
View File
@@ -0,0 +1,191 @@
// battery Manager
// Purpose: Defines the battery Manager module and the helpers/state used by this service unit.
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
const logger = require('../../globals/logger').child('batteryManager');
const { publishEvent } = require('../eventBus');
const { managerEvents, lockRover, rovers } = require('../roverManager');
const STATES = {
NORMAL: 'normal',
WARN: 'warn',
URGENT: 'urgent',
DOCKED: 'docked',
CHARGING: 'charging',
FULL: 'full',
LOCKED: 'locked',
};
const WAITING_UNLOCK_MS = 5 * 60 * 1000;
const roverState = new Map(); // roverId -> state snapshot
function getState(roverId) {
if (!roverState.has(roverId)) {
roverState.set(roverId, {
lastPercent: null,
warned: false,
urgent: false,
onDock: false,
dockedCharging: false,
charging: false,
batteryLocked: false,
waitingSince: null,
});
}
return roverState.get(roverId);
}
function shouldLock(record) {
return record.lockReason == null || record.lockReason === 'battery';
}
function handleSensorEvent({ roverId, sensors, batteryState }) {
if (!roverId) return;
const record = rovers.get(roverId);
if (!record) return;
const state = getState(roverId);
const config = record.meta?.battery || {};
const lockable = shouldLock(record);
if (batteryState?.percent != null) {
state.lastPercent = batteryState.percent;
}
if (!state.warned && batteryState?.warnActive) {
state.warned = true;
publishEvent({
source: 'batteryManager',
type: 'battery.warn',
payload: { roverId, batteryState },
});
}
if (!state.urgent && batteryState?.urgentActive) {
state.urgent = true;
publishEvent({
source: 'batteryManager',
type: 'battery.urgent',
payload: { roverId, batteryState },
});
}
const chargingState = sensors?.chargingState?.code;
const chargingSources = sensors?.chargingSources;
const onDock = Boolean(chargingSources?.homeBase);
if (onDock && !state.onDock) {
state.onDock = true;
publishEvent({
source: 'batteryManager',
type: 'battery.docked',
payload: { roverId, batteryState },
});
} else if (!onDock && state.onDock) {
state.onDock = false;
publishEvent({
source: 'batteryManager',
type: 'battery.undocked',
payload: { roverId, batteryState },
});
}
const chargingLabel = sensors?.chargingState?.label?.toLowerCase();
const chargingByLabel =
chargingLabel === 'waiting' || chargingLabel === 'full charging' || chargingLabel === 'trickle charging';
const chargingByCode = chargingState === 2 || chargingState === 3 || chargingState === 4;
const isCharging = chargingByLabel || chargingByCode;
if (isCharging && !state.charging) {
state.charging = true;
publishEvent({
source: 'batteryManager',
type: 'battery.charging.start',
payload: { roverId, batteryState },
});
} else if (!isCharging && state.charging) {
state.charging = false;
publishEvent({
source: 'batteryManager',
type: 'battery.charging.stop',
payload: { roverId, batteryState },
});
}
const dockedCharging = onDock && isCharging;
if (dockedCharging && !state.dockedCharging) {
state.dockedCharging = true;
} else if (!dockedCharging && state.dockedCharging) {
state.dockedCharging = false;
}
const waitingState = chargingState === 4;
if (waitingState && state.waitingSince == null) {
state.waitingSince = Date.now();
} else if (!waitingState && state.waitingSince != null) {
state.waitingSince = null;
}
const waitingLongEnough =
waitingState && state.waitingSince != null && Date.now() - state.waitingSince >= WAITING_UNLOCK_MS;
const warnThreshold =
typeof config?.Warn === 'number' ? config.Warn : batteryState?.warn ?? null;
const fullThreshold =
typeof config?.Full === 'number' ? config.Full : batteryState?.full ?? null;
const chargeMah = batteryState?.charge ?? null;
let waitingPercent = batteryState?.percent ?? null;
if (chargeMah != null && warnThreshold != null && fullThreshold != null && fullThreshold > warnThreshold) {
waitingPercent = (chargeMah - warnThreshold) / (fullThreshold - warnThreshold);
waitingPercent = Math.max(0, Math.min(1, waitingPercent));
}
const isFull =
(batteryState?.charge != null && fullThreshold != null && batteryState.charge >= fullThreshold) ||
(batteryState?.percent != null && batteryState.percent >= 0.99);
const needsCharge = state.warned;
if (dockedCharging && lockable && needsCharge && !state.batteryLocked && !isFull && !waitingLongEnough) {
logger.info('Auto-locking rover for charging', { roverId });
lockRover(roverId, true, { reason: 'battery' });
state.batteryLocked = true;
publishEvent({
source: 'batteryManager',
type: 'battery.locked',
payload: { roverId, batteryState },
});
}
const waitingHalfOrMore = waitingPercent != null && waitingPercent >= 0.01;
// const waitingHalfOrMore = true;
const shouldUnlock = state.batteryLocked && waitingLongEnough && waitingHalfOrMore;
if (shouldUnlock) {
logger.info('Unlocking rover after charge', {
roverId,
isFull,
docked: dockedCharging,
waitingState,
waitingPercent,
waitedMs: state.waitingSince ? Date.now() - state.waitingSince : null,
});
lockRover(roverId, false, { reason: 'battery' });
state.batteryLocked = false;
state.warned = false;
state.urgent = false;
state.waitingSince = null;
publishEvent({
source: 'batteryManager',
type: 'battery.unlocked',
payload: { roverId, batteryState },
});
}
}
function handleLockEvent({ roverId, locked, reason }) {
const state = getState(roverId);
if (!state) return;
if (!locked) {
state.batteryLocked = false;
state.waitingSince = null;
return;
}
state.batteryLocked = reason === 'battery';
}
managerEvents.on('sensor', handleSensorEvent);
managerEvents.on('lock', handleLockEvent);