mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
headlight rework and laser addition
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -78,7 +78,7 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-BoGJudGZ.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BoLmfZBk.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CKlOAshP.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -2,19 +2,13 @@
|
||||
// Purpose: Defines the darkness reward that alters visibility/lighting behavior. Scope: Encapsulates reward metadata and effect configuration for runtime execution.
|
||||
const DURATION_MS = 15 * 60 * 1000;
|
||||
const LIGHT_ENFORCE_TICK_MS = 3000;
|
||||
// Rover daemon semantics are inverted:
|
||||
// action "on" => IR LED on => nightVisionOn=false
|
||||
// action "off" => IR LED off => nightVisionOn=true
|
||||
function actionForNightVisionState(nightVisionOn) {
|
||||
return nightVisionOn ? 'off' : 'on';
|
||||
}
|
||||
|
||||
let activeTimer = null;
|
||||
let enforceLightsTimer = null;
|
||||
let nightVisionLockUntil = 0;
|
||||
let headlightLockUntil = 0;
|
||||
|
||||
function isNightVisionBlocked() {
|
||||
return Date.now() < nightVisionLockUntil;
|
||||
function isHeadlightBlocked() {
|
||||
return Date.now() < headlightLockUntil;
|
||||
}
|
||||
|
||||
function clearTimers() {
|
||||
@@ -43,7 +37,7 @@ async function forceAllLightsOff(ctx) {
|
||||
|
||||
async function stopDarkness(ctx, effect = {}) {
|
||||
clearTimers();
|
||||
nightVisionLockUntil = 0;
|
||||
headlightLockUntil = 0;
|
||||
|
||||
const prevLights = Array.isArray(effect.prevLights) ? effect.prevLights : [];
|
||||
await Promise.all(
|
||||
@@ -73,17 +67,17 @@ async function stopDarkness(ctx, effect = {}) {
|
||||
ctx.logger.warn('darkness restore light lock failed', { error: err.message });
|
||||
}
|
||||
|
||||
const prevNightVision = effect.prevNightVision && typeof effect.prevNightVision === 'object'
|
||||
? effect.prevNightVision
|
||||
const prevHeadlights = effect.prevHeadlights && typeof effect.prevHeadlights === 'object'
|
||||
? effect.prevHeadlights
|
||||
: {};
|
||||
Object.entries(prevNightVision).forEach(([roverId, wasOn]) => {
|
||||
Object.entries(prevHeadlights).forEach(([roverId, wasOn]) => {
|
||||
try {
|
||||
ctx.issueCommand(String(roverId), {
|
||||
type: 'nightVision',
|
||||
nightVision: { action: actionForNightVisionState(Boolean(wasOn)) },
|
||||
type: 'headlight',
|
||||
headlight: { action: Boolean(wasOn) ? 'on' : 'off' },
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.logger.warn('darkness restore nightVision failed', { roverId, error: err.message });
|
||||
ctx.logger.warn('darkness restore headlight failed', { roverId, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,7 +88,7 @@ async function startDarkness(ctx, effect) {
|
||||
clearTimers();
|
||||
const endsAt = Number(effect.endsAt || Date.now() + DURATION_MS);
|
||||
const remaining = Math.max(0, endsAt - Date.now());
|
||||
nightVisionLockUntil = endsAt;
|
||||
headlightLockUntil = endsAt;
|
||||
try {
|
||||
await ctx.setHomeAssistantLightsLockedOn(true, {
|
||||
source: 'buttonbox:darkness',
|
||||
@@ -122,31 +116,31 @@ async function startDarkness(ctx, effect) {
|
||||
module.exports = {
|
||||
id: 'darkness',
|
||||
name: 'Darkness',
|
||||
isNightVisionBlocked,
|
||||
isHeadlightBlocked,
|
||||
goal: 400,
|
||||
async run(ctx) {
|
||||
const entities = ctx.getHomeAssistantEntities();
|
||||
const prevLights = entities.map((entity) => ({ id: entity.id, state: entity.state === 'on' ? 'on' : 'off' }));
|
||||
await forceAllLightsOff(ctx);
|
||||
|
||||
const prevNightVision = {};
|
||||
const prevHeadlights = {};
|
||||
ctx.listOnlineRovers().forEach((rover) => {
|
||||
const state = rover?.nightVision?.state;
|
||||
const nightVisionOn = Boolean(state && state.nightVisionOn === true);
|
||||
prevNightVision[String(rover.id)] = nightVisionOn;
|
||||
const state = rover?.headlight?.state;
|
||||
const headlightOn = Boolean(state && state.headlightOn === true);
|
||||
prevHeadlights[String(rover.id)] = headlightOn;
|
||||
try {
|
||||
ctx.issueCommand(String(rover.id), {
|
||||
type: 'nightVision',
|
||||
nightVision: { action: actionForNightVisionState(false) },
|
||||
type: 'headlight',
|
||||
headlight: { action: 'off' },
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.logger.warn('darkness nightVision off failed', { roverId: rover.id, error: err.message });
|
||||
ctx.logger.warn('darkness headlight off failed', { roverId: rover.id, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
const prevPolicy = ctx.getHomeAssistantLightPolicy?.() || null;
|
||||
const prevLightLockState = prevPolicy?.lockState || (prevPolicy?.lockedOn ? 'on' : null);
|
||||
const effect = { endsAt: Date.now() + DURATION_MS, prevLights, prevNightVision, prevLightLockState };
|
||||
const effect = { endsAt: Date.now() + DURATION_MS, prevLights, prevHeadlights, prevLightLockState };
|
||||
await startDarkness(ctx, effect);
|
||||
ctx.sendAlert({ color: '#212121', title: 'Darkness', message: 'Darkness effect active for 120 seconds.' });
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ const roverManager = require('../roverManager');
|
||||
const { isAdmin, isLockdownAdmin } = require('../roleService');
|
||||
const { isDeterred } = require('../verificationService');
|
||||
const logger = require('../../globals/logger').child('commandService');
|
||||
const { isNightVisionBlocked } = require('../../rewards/definitions/darkness');
|
||||
const { isHeadlightBlocked } = require('../../rewards/definitions/darkness');
|
||||
|
||||
const pendingCommands = new Map(); // id -> { roverId }
|
||||
const lastDriveActivity = new Map(); // roverId -> { ts, socketId, direction, speed, isAdmin }
|
||||
@@ -132,7 +132,7 @@ function shouldRecordTurnActivity(type, payload = {}) {
|
||||
|
||||
/*
|
||||
Other accepted commands are left as activity because they are tied to an
|
||||
explicit user control: servo moves, night vision toggles, raw OI commands,
|
||||
explicit user control: servo moves, headlight/laser toggles, raw OI commands,
|
||||
songs, reboot/update admin actions, and similar commands.
|
||||
*/
|
||||
return true;
|
||||
@@ -158,8 +158,8 @@ io.on('connection', (socket) => {
|
||||
if (type === 'audioLevels') {
|
||||
throw new Error('audioLevels command is service-managed');
|
||||
}
|
||||
if (type === 'nightVision' && isNightVisionBlocked()) {
|
||||
logger.info('Ignoring night vision command while darkness lock is active', { socketId: socket.id, roverId });
|
||||
if (type === 'headlight' && isHeadlightBlocked()) {
|
||||
logger.info('Ignoring headlight command while darkness lock is active', { socketId: socket.id, roverId });
|
||||
reply({ ignored: true, reason: 'darknessActive' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const homeAssistantService = require('../homeAssistantService');
|
||||
const neatoService = require('../neatoService');
|
||||
const liftService = require('../liftService');
|
||||
const {
|
||||
NIGHT_VISION_DISABLE_ACTION,
|
||||
HEADLIGHT_DISABLE_ACTION,
|
||||
DOCK_COMMAND_BASE64,
|
||||
} = require('./constants');
|
||||
|
||||
@@ -60,7 +60,7 @@ async function dockAllRovers() {
|
||||
return { action: 'dockAllRovers', attempted, failed };
|
||||
}
|
||||
|
||||
async function disableAllRoverNightVision() {
|
||||
async function disableAllRoverHeadlights() {
|
||||
const attempted = [];
|
||||
const failed = [];
|
||||
roverManager.rovers.forEach((record) => {
|
||||
@@ -68,15 +68,15 @@ async function disableAllRoverNightVision() {
|
||||
const roverId = String(record.id);
|
||||
try {
|
||||
issueCommand(roverId, {
|
||||
type: 'nightVision',
|
||||
nightVision: { action: NIGHT_VISION_DISABLE_ACTION },
|
||||
type: 'headlight',
|
||||
headlight: { action: HEADLIGHT_DISABLE_ACTION },
|
||||
});
|
||||
attempted.push(roverId);
|
||||
} catch (err) {
|
||||
failed.push({ roverId, error: err.message });
|
||||
}
|
||||
});
|
||||
return { action: 'disableRoverNightVision', attempted, failed };
|
||||
return { action: 'disableRoverHeadlights', attempted, failed };
|
||||
}
|
||||
|
||||
async function sendNeatoHome() {
|
||||
@@ -100,7 +100,7 @@ async function raiseLift() {
|
||||
const idleActions = [
|
||||
turnOffRoomControls,
|
||||
// dockAllRovers,
|
||||
disableAllRoverNightVision,
|
||||
disableAllRoverHeadlights,
|
||||
sendNeatoHome,
|
||||
raiseLift,
|
||||
];
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Purpose: Defines idle timing and command constants used by idle automation workflows.
|
||||
// Scope: Centralizes immutable configuration for trigger windows and rover command payloads.
|
||||
const IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
||||
const NIGHT_VISION_DISABLE_ACTION = 'on';
|
||||
const HEADLIGHT_DISABLE_ACTION = 'off';
|
||||
const DOCK_COMMAND_BASE64 = Buffer.from([143]).toString('base64');
|
||||
|
||||
module.exports = {
|
||||
IDLE_TIMEOUT_MS,
|
||||
NIGHT_VISION_DISABLE_ACTION,
|
||||
HEADLIGHT_DISABLE_ACTION,
|
||||
DOCK_COMMAND_BASE64,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,24 @@ function coerceBool(value) {
|
||||
|
||||
const HEARTBEAT_INTERVAL_MS = 15000;
|
||||
|
||||
function handleToggleEvent(roverId, msg) {
|
||||
if (msg.event === 'headlight.state') {
|
||||
const headlightOn = coerceBool(msg.data?.headlightOn);
|
||||
if (headlightOn != null) {
|
||||
roverManager.setToggleState(roverId, 'headlight', headlightOn);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (msg.event === 'laser.state') {
|
||||
const laserOn = coerceBool(msg.data?.laserOn);
|
||||
if (laserOn != null) {
|
||||
roverManager.setToggleState(roverId, 'laser', laserOn);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleMessage(roverId, msg) {
|
||||
switch (msg.type) {
|
||||
case 'hello':
|
||||
@@ -38,9 +56,7 @@ function handleMessage(roverId, msg) {
|
||||
roverManager.handleHostStats(roverId, msg);
|
||||
break;
|
||||
case 'event': {
|
||||
const nightVisionOn = coerceBool(msg.data?.nightVisionOn);
|
||||
if (msg.event === 'nightVision.state' && nightVisionOn != null) {
|
||||
roverManager.setNightVisionState(roverId, nightVisionOn);
|
||||
if (handleToggleEvent(roverId, msg)) {
|
||||
break;
|
||||
}
|
||||
sendAlert({ color: ALERT_COLOR, title: `${roverId} event`, message: msg.event });
|
||||
@@ -100,10 +116,7 @@ roverWSS.on('connection', (ws) => {
|
||||
} else if (msg.type === 'ack') {
|
||||
handleAck(msg);
|
||||
} else if (msg.type === 'event') {
|
||||
const nightVisionOn = coerceBool(msg.data?.nightVisionOn);
|
||||
if (msg.event === 'nightVision.state' && nightVisionOn != null) {
|
||||
roverManager.setNightVisionState(roverId, nightVisionOn);
|
||||
} else {
|
||||
if (!handleToggleEvent(roverId, msg)) {
|
||||
sendAlert({ color: ALERT_COLOR, title: `${roverId}`, message: msg.event });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ const {
|
||||
getRoster,
|
||||
getRosterForSocket,
|
||||
broadcastRoster,
|
||||
setNightVisionState,
|
||||
setToggleState,
|
||||
handleHostStats,
|
||||
canSeeRover,
|
||||
canRequestControl,
|
||||
@@ -261,7 +261,7 @@ module.exports = {
|
||||
getRoster,
|
||||
getRosterForSocket,
|
||||
broadcastRoster,
|
||||
setNightVisionState,
|
||||
setToggleState,
|
||||
handleHostStats,
|
||||
handleSensorFrame,
|
||||
requestControl,
|
||||
|
||||
@@ -40,7 +40,8 @@ function createRosterLifecycle(deps) {
|
||||
locked: false,
|
||||
lockReason: null,
|
||||
batteryState: null,
|
||||
nightVisionState: null,
|
||||
headlightState: null,
|
||||
laserState: null,
|
||||
room: `rover:${id}`,
|
||||
lastSeen: Date.now(),
|
||||
lastMovementAt: Date.now(),
|
||||
@@ -73,10 +74,19 @@ function createRosterLifecycle(deps) {
|
||||
} else {
|
||||
record.privateOpen = true;
|
||||
}
|
||||
if (record.nightVisionState == null && meta?.nightVision?.enabled) {
|
||||
const ledOn = Boolean(meta.nightVision.initialOn);
|
||||
record.nightVisionState = {
|
||||
nightVisionOn: !ledOn,
|
||||
if (!meta?.headlight?.enabled) {
|
||||
record.headlightState = null;
|
||||
} else if (record.headlightState == null) {
|
||||
record.headlightState = {
|
||||
headlightOn: Boolean(meta.headlight.initialOn),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
if (!meta?.laser?.enabled) {
|
||||
record.laserState = null;
|
||||
} else if (record.laserState == null) {
|
||||
record.laserState = {
|
||||
laserOn: Boolean(meta.laser.initialOn),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
@@ -221,9 +231,12 @@ function createRosterLifecycle(deps) {
|
||||
cameraServo: record.meta?.cameraServo,
|
||||
audio: record.meta?.audio,
|
||||
horn: record.meta?.horn,
|
||||
nightVision: record.meta?.nightVision
|
||||
? { ...record.meta.nightVision, state: record.nightVisionState }
|
||||
: record.meta?.nightVision,
|
||||
headlight: record.meta?.headlight
|
||||
? { ...record.meta.headlight, state: record.headlightState }
|
||||
: record.meta?.headlight,
|
||||
laser: record.meta?.laser
|
||||
? { ...record.meta.laser, state: record.laserState }
|
||||
: record.meta?.laser,
|
||||
locked: record.locked || (isPrivateRecord(record) && !isPrivateOpen(record)),
|
||||
lockReason: record.lockReason || (isPrivateRecord(record) && !isPrivateOpen(record) ? 'private' : null),
|
||||
lastSeen: record.lastSeen,
|
||||
@@ -258,16 +271,19 @@ function createRosterLifecycle(deps) {
|
||||
});
|
||||
}
|
||||
|
||||
function setNightVisionState(roverId, nightVisionOn) {
|
||||
function setToggleState(roverId, device, on) {
|
||||
const record = rovers.get(roverId);
|
||||
if (!record) return;
|
||||
if (typeof nightVisionOn !== 'boolean') return;
|
||||
record.nightVisionState = {
|
||||
nightVisionOn,
|
||||
if (typeof on !== 'boolean') return;
|
||||
if (device !== 'headlight' && device !== 'laser') return;
|
||||
const stateKey = device === 'headlight' ? 'headlightState' : 'laserState';
|
||||
const onKey = `${device}On`;
|
||||
record[stateKey] = {
|
||||
[onKey]: on,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
broadcastRoster();
|
||||
managerEvents.emit('rover', { roverId, action: 'nightVision', record });
|
||||
managerEvents.emit('rover', { roverId, action: device, record });
|
||||
}
|
||||
|
||||
function handleHostStats(roverId, msg = {}) {
|
||||
@@ -316,7 +332,7 @@ function createRosterLifecycle(deps) {
|
||||
getRosterForSocket,
|
||||
syncSpectatorRooms,
|
||||
broadcastRoster,
|
||||
setNightVisionState,
|
||||
setToggleState,
|
||||
handleHostStats,
|
||||
canSeeRover,
|
||||
canRequestControl,
|
||||
|
||||
@@ -10,7 +10,7 @@ const serverTimezone = config.timezone || null;
|
||||
const configuredSocials = Array.isArray(config.socials) ? config.socials : null;
|
||||
|
||||
const ACTIVITY_SYNC_COOLDOWN_MS = 3000;
|
||||
const NIGHT_VISION_SYNC_COOLDOWN_MS = 1000;
|
||||
const GPIO_TOGGLE_SYNC_COOLDOWN_MS = 1000;
|
||||
const PERIODIC_SYNC_MS = 20000;
|
||||
|
||||
module.exports = {
|
||||
@@ -19,6 +19,6 @@ module.exports = {
|
||||
serverTimezone,
|
||||
configuredSocials,
|
||||
ACTIVITY_SYNC_COOLDOWN_MS,
|
||||
NIGHT_VISION_SYNC_COOLDOWN_MS,
|
||||
GPIO_TOGGLE_SYNC_COOLDOWN_MS,
|
||||
PERIODIC_SYNC_MS,
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ const {
|
||||
serverTimezone,
|
||||
configuredSocials,
|
||||
ACTIVITY_SYNC_COOLDOWN_MS,
|
||||
NIGHT_VISION_SYNC_COOLDOWN_MS,
|
||||
GPIO_TOGGLE_SYNC_COOLDOWN_MS,
|
||||
PERIODIC_SYNC_MS,
|
||||
} = require('./constants');
|
||||
const { getState, setState } = require('./state');
|
||||
@@ -175,29 +175,29 @@ modeEvents.on('change', () => {
|
||||
|
||||
managerEvents.on('rover', (event = {}) => {
|
||||
const state = getState();
|
||||
if (event.action === 'nightVision') {
|
||||
if (event.action === 'headlight' || event.action === 'laser') {
|
||||
const now = Date.now();
|
||||
const elapsed = now - state.lastNightVisionSync;
|
||||
if (elapsed >= NIGHT_VISION_SYNC_COOLDOWN_MS) {
|
||||
setState({ lastNightVisionSync: now });
|
||||
logger.info('Night vision update; syncing all clients (immediate)');
|
||||
const elapsed = now - state.lastGPIOToggleSync;
|
||||
if (elapsed >= GPIO_TOGGLE_SYNC_COOLDOWN_MS) {
|
||||
setState({ lastGPIOToggleSync: now });
|
||||
logger.info('GPIO toggle update; syncing all clients (immediate)');
|
||||
syncAll();
|
||||
return;
|
||||
}
|
||||
if (!state.pendingNightVisionSync) {
|
||||
const delay = NIGHT_VISION_SYNC_COOLDOWN_MS - elapsed;
|
||||
if (!state.pendingGPIOToggleSync) {
|
||||
const delay = GPIO_TOGGLE_SYNC_COOLDOWN_MS - elapsed;
|
||||
const timer = setTimeout(() => {
|
||||
setState({ lastNightVisionSync: Date.now(), pendingNightVisionSync: null });
|
||||
logger.info('Night vision update; syncing all clients (delayed)');
|
||||
setState({ lastGPIOToggleSync: Date.now(), pendingGPIOToggleSync: null });
|
||||
logger.info('GPIO toggle update; syncing all clients (delayed)');
|
||||
syncAll();
|
||||
}, delay);
|
||||
setState({ pendingNightVisionSync: timer });
|
||||
setState({ pendingGPIOToggleSync: timer });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (state.pendingNightVisionSync) {
|
||||
clearTimeout(state.pendingNightVisionSync);
|
||||
setState({ pendingNightVisionSync: null });
|
||||
if (state.pendingGPIOToggleSync) {
|
||||
clearTimeout(state.pendingGPIOToggleSync);
|
||||
setState({ pendingGPIOToggleSync: null });
|
||||
}
|
||||
logger.info('Rover roster change; syncing all clients');
|
||||
syncAll();
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
// Scope: Keeps runtime behavior unchanged while centralizing mutable session-sync state in one module.
|
||||
let lastActivitySync = 0;
|
||||
let pendingActivitySync = null;
|
||||
let lastNightVisionSync = 0;
|
||||
let pendingNightVisionSync = null;
|
||||
let lastGPIOToggleSync = 0;
|
||||
let pendingGPIOToggleSync = null;
|
||||
|
||||
function getState() {
|
||||
return {
|
||||
lastActivitySync,
|
||||
pendingActivitySync,
|
||||
lastNightVisionSync,
|
||||
pendingNightVisionSync,
|
||||
lastGPIOToggleSync,
|
||||
pendingGPIOToggleSync,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ function setState(patch = {}) {
|
||||
if (Object.prototype.hasOwnProperty.call(patch, 'pendingActivitySync')) {
|
||||
pendingActivitySync = patch.pendingActivitySync;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, 'lastNightVisionSync')) {
|
||||
lastNightVisionSync = patch.lastNightVisionSync;
|
||||
if (Object.prototype.hasOwnProperty.call(patch, 'lastGPIOToggleSync')) {
|
||||
lastGPIOToggleSync = patch.lastGPIOToggleSync;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(patch, 'pendingNightVisionSync')) {
|
||||
pendingNightVisionSync = patch.pendingNightVisionSync;
|
||||
if (Object.prototype.hasOwnProperty.call(patch, 'pendingGPIOToggleSync')) {
|
||||
pendingGPIOToggleSync = patch.pendingGPIOToggleSync;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user