Files
MultiRoombaRover/server/src/services/buttonBoxService/index.js
T
2026-09-14 17:56:53 -04:00

122 lines
4.0 KiB
JavaScript

// button Box Service
// Purpose: Composes button-box storage, core reward workflows, and HTTP route wiring.
// Scope: Keeps runtime behavior unchanged while making this entrypoint a thin composition layer.
const { app } = require('../../globals/http');
const io = require('../../globals/io');
const logger = require('../../globals/logger').child('buttonBoxService');
const { loadConfig, registerConfigurationHandler } = require('../../configuration');
const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
const { publishEvent } = require('../eventBus');
const { getRewardById, listRewards } = require('../../rewards');
const roverManager = require('../roverManager');
const { issueCommand } = require('../commandService');
const { sendAlert } = require('../alertService');
const { sendExternalTyping, sendExternalMessage, sendSystemMessage } = require('../chatService');
const { setMode, getMode } = require('../modeManager');
const { getAdminReason, setAdminReason, clearAdminReason } = require('../adminReasonService');
const assignmentService = require('../assignmentService');
const {
getState: getHomeAssistantState,
setEntityState: setHomeAssistantEntityState,
setLightsLockedOn: setHomeAssistantLightsLockedOn,
} = require('../homeAssistantService');
const { isLocalNetwork, normalizeIp } = require('../../helpers/ipResolver');
const { createButtonBoxStore } = require('./store');
const { createButtonBoxCore } = require('./core');
const { registerButtonBoxRoute } = require('./httpRoute');
const greenModeService = require('../greenModeService');
const DATA_DIR = resolveDataDir();
const STORE_PATH = resolveDataPath('buttonbox-state.json');
const BUTTON_COUNT = 4;
const STORE_VERSION = 1;
let enabled = Boolean(loadConfig().buttonBox?.enabled);
const store = createButtonBoxStore({
logger,
getRewardById,
listRewards,
dataDir: DATA_DIR,
storePath: STORE_PATH,
buttonCount: BUTTON_COUNT,
storeVersion: STORE_VERSION,
});
const core = createButtonBoxCore({
io,
logger,
getRewardById,
roverManager,
issueCommand,
sendAlert,
publishEvent,
sendExternalTyping,
sendExternalMessage,
sendSystemMessage,
getMode,
setMode,
getAdminReason,
setAdminReason,
clearAdminReason,
assignmentService,
getHomeAssistantState,
setHomeAssistantEntityState,
setHomeAssistantLightsLockedOn,
setGreenMode: greenModeService.setEnabled,
isGreenModeEnabled: greenModeService.isEnabled,
// Return an explicit cleanup function so timed rewards can stop observing
// the global service when they expire, rerun, or are recovered.
onGreenModeChange: (listener) => {
greenModeService.greenModeEvents.on('change', listener);
return () => greenModeService.greenModeEvents.off('change', listener);
},
store,
});
registerButtonBoxRoute({
app,
logger,
buttonCount: BUTTON_COUNT,
normalizeIp,
isLocalNetwork,
isEnabled: () => enabled,
applyPress: core.applyPress,
});
function enableButtonBox() {
store.loadState();
core.recoverEffects().catch((err) => {
logger.warn('Button box effect recovery failed', err.message);
});
}
if (enabled) {
enableButtonBox();
} else {
logger.info('Button box disabled by config');
}
registerConfigurationHandler('buttonBox', (buttonBoxConfig = {}) => {
const wasEnabled = enabled;
enabled = Boolean(buttonBoxConfig.enabled);
// Persistent state is loaded only on the transition to enabled. The core has
// no long-running hardware client, so disabling is completely represented by
// the route and public-method gates.
if (!wasEnabled && enabled) enableButtonBox();
});
module.exports = {
getButtonBoxState: () => {
/*
Session sync still includes a buttonBox key for a stable payload shape,
but disabled mode must not create/read the persisted button-box store.
*/
if (!enabled) return { buttons: [] };
return store.getStateClone();
},
addButtonBoxCount: (...args) => {
if (!enabled) throw new Error('Button box is disabled');
return core.addCount(...args);
},
};