Files
MultiRoombaRover/server/src/services/roverManager/mathUtils.js
T
2026-04-28 20:32:15 -04:00

139 lines
4.0 KiB
JavaScript

// rover Manager math utils
// Purpose: Centralizes numeric clamping and battery display calculations used by rover-manager orchestration.
// Scope: Keeps runtime behavior unchanged by extracting pure utility functions from the main service module.
const { DEFAULT_PRIVATE_SAFETY } = require('./constants');
function clampInt(value, min, max, fallback) {
const num = Number.parseInt(value, 10);
if (!Number.isFinite(num)) return fallback;
return Math.max(min, Math.min(max, num));
}
function normalizePrivateSafety(raw = {}) {
const source = raw && typeof raw === 'object' ? raw : {};
return {
speedLimitEnabled: Boolean(source.speedLimitEnabled),
speedLimitMaxWheelSpeed: clampInt(
source.speedLimitMaxWheelSpeed,
1,
500,
DEFAULT_PRIVATE_SAFETY.speedLimitMaxWheelSpeed,
),
hardOvercurrentEnabled: Boolean(source.hardOvercurrentEnabled),
overcurrentStopMs: clampInt(
source.overcurrentStopMs,
100,
5000,
DEFAULT_PRIVATE_SAFETY.overcurrentStopMs,
),
hardBumpEnabled: Boolean(source.hardBumpEnabled),
bumpBackoffSpeed: clampInt(
source.bumpBackoffSpeed,
1,
500,
DEFAULT_PRIVATE_SAFETY.bumpBackoffSpeed,
),
bumpBackoffMs: clampInt(
source.bumpBackoffMs,
100,
5000,
DEFAULT_PRIVATE_SAFETY.bumpBackoffMs,
),
cliffEnabled: Boolean(source.cliffEnabled),
cliffBackoffSpeed: clampInt(
source.cliffBackoffSpeed,
1,
500,
DEFAULT_PRIVATE_SAFETY.cliffBackoffSpeed,
),
cliffBackoffMs: clampInt(
source.cliffBackoffMs,
100,
5000,
DEFAULT_PRIVATE_SAFETY.cliffBackoffMs,
),
triggerCooldownMs: clampInt(
source.triggerCooldownMs,
100,
10000,
DEFAULT_PRIVATE_SAFETY.triggerCooldownMs,
),
};
}
function computeBatteryDisplayPercent({ charge, full, warn, urgent, percent, capacity }) {
if (
charge != null &&
full != null &&
warn != null &&
urgent != null &&
full > warn &&
warn > urgent
) {
if (charge <= urgent) return 0;
if (charge <= warn) {
const t = (charge - urgent) / (warn - urgent);
return Math.round(Math.max(0, Math.min(1, t)) * 10);
}
if (charge >= full) return 100;
const t = (charge - warn) / (full - warn);
return Math.round((0.1 + Math.max(0, Math.min(1, t)) * 0.9) * 100);
}
if (percent != null && Number.isFinite(percent)) {
return Math.round(Math.max(0, Math.min(1, percent)) * 100);
}
if (charge != null && capacity != null && capacity > 0) {
const fallback = charge / capacity;
return Math.round(Math.max(0, Math.min(1, fallback)) * 100);
}
return null;
}
function computeBatteryState(record, sensors) {
if (!record) return null;
if (!sensors) return record.batteryState;
const config = record.meta?.battery || null;
const charge = sensors?.batteryChargeMah ?? null;
const capacity = sensors?.batteryCapacityMah ?? null;
const full = typeof config?.Full === 'number' ? config.Full : null;
const warn = typeof config?.Warn === 'number' ? config.Warn : null;
const urgent = typeof config?.Urgent === 'number' ? config.Urgent : null;
let percent = null;
if (charge != null && warn != null && full != null && full > warn) {
const span = full - warn;
percent = (charge - warn) / span;
} else if (charge != null && capacity != null && capacity > 0) {
percent = charge / capacity;
}
if (percent != null) {
percent = Math.max(0, Math.min(1, percent));
}
const percentDisplay = computeBatteryDisplayPercent({
charge,
full,
warn,
urgent,
percent,
capacity,
});
return {
charge,
capacity,
full,
warn,
urgent,
percent,
percentDisplay,
warnActive: Boolean(warn != null && charge != null && charge <= warn),
urgentActive: Boolean(urgent != null && charge != null && charge <= urgent),
updatedAt: Date.now(),
};
}
module.exports = {
clampInt,
normalizePrivateSafety,
computeBatteryDisplayPercent,
computeBatteryState,
};