mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
laser locking and idle
This commit is contained in:
File diff suppressed because one or more lines are too long
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-BoLmfZBk.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C_cLKG0x.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CKlOAshP.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -8,10 +8,53 @@ const { isAdmin, isLockdownAdmin } = require('../roleService');
|
||||
const { isDeterred } = require('../verificationService');
|
||||
const logger = require('../../globals/logger').child('commandService');
|
||||
const { isHeadlightBlocked } = require('../../rewards/definitions/darkness');
|
||||
const homeAssistantService = require('../homeAssistantService');
|
||||
|
||||
const pendingCommands = new Map(); // id -> { roverId }
|
||||
const lastDriveActivity = new Map(); // roverId -> { ts, socketId, direction, speed, isAdmin }
|
||||
const driveCooldowns = new Map(); // roverId -> blockedUntil
|
||||
let roomLightsWereLockedOn = Boolean(homeAssistantService.getLightPolicyState?.()?.lockedOn);
|
||||
|
||||
function isRoomLightsLockedOn() {
|
||||
return Boolean(homeAssistantService.getLightPolicyState?.()?.lockedOn);
|
||||
}
|
||||
|
||||
function getLaserAction(payload = {}) {
|
||||
return String(payload?.laser?.action || 'toggle').trim().toLowerCase() || 'toggle';
|
||||
}
|
||||
|
||||
function isLaserCommandBlockedByRoomLightLock(payload = {}) {
|
||||
if (!isRoomLightsLockedOn()) return false;
|
||||
// A locked-on room-light policy means the laser must not emit. Explicit off
|
||||
// commands are still allowed so cleanup paths can force a safe state.
|
||||
return getLaserAction(payload) !== 'off';
|
||||
}
|
||||
|
||||
function forceAllRoverLasersOff(reason) {
|
||||
const attempted = [];
|
||||
const failed = [];
|
||||
roverManager.rovers.forEach((record) => {
|
||||
if (!record?.ws || !record?.meta?.laser?.enabled) return;
|
||||
const roverId = String(record.id);
|
||||
try {
|
||||
issueCommand(roverId, {
|
||||
type: 'laser',
|
||||
laser: { action: 'off' },
|
||||
});
|
||||
attempted.push(roverId);
|
||||
} catch (err) {
|
||||
failed.push({ roverId, error: err.message });
|
||||
}
|
||||
});
|
||||
if (attempted.length || failed.length) {
|
||||
logger.info('Forced rover lasers off', { reason, attempted, failed });
|
||||
}
|
||||
}
|
||||
|
||||
function enforceLaserRoomLightLock(reason) {
|
||||
if (!isRoomLightsLockedOn()) return;
|
||||
forceAllRoverLasersOff(reason);
|
||||
}
|
||||
|
||||
function normalizeOutboundCommandPayload(payload = {}) {
|
||||
if (payload?.type !== 'tts') return payload;
|
||||
@@ -38,6 +81,9 @@ function issueCommand(roverId, payload) {
|
||||
if (!record || !record.ws) {
|
||||
throw new Error('Rover offline');
|
||||
}
|
||||
if (payload?.type === 'laser' && isLaserCommandBlockedByRoomLightLock(payload)) {
|
||||
throw new Error('Laser disabled while room lights are locked on');
|
||||
}
|
||||
const id = uuidv4();
|
||||
const normalizedPayload = normalizeOutboundCommandPayload(payload);
|
||||
const message = { ...normalizedPayload, id };
|
||||
@@ -158,12 +204,21 @@ io.on('connection', (socket) => {
|
||||
if (type === 'audioLevels') {
|
||||
throw new Error('audioLevels command is service-managed');
|
||||
}
|
||||
const payload = data ? { ...data } : {};
|
||||
if (type === 'headlight' && isHeadlightBlocked()) {
|
||||
logger.info('Ignoring headlight command while darkness lock is active', { socketId: socket.id, roverId });
|
||||
reply({ ignored: true, reason: 'darknessActive' });
|
||||
return;
|
||||
}
|
||||
const payload = data ? { ...data } : {};
|
||||
if (type === 'laser' && isLaserCommandBlockedByRoomLightLock(payload)) {
|
||||
logger.info('Ignoring laser command while room lights are locked on', {
|
||||
socketId: socket.id,
|
||||
roverId,
|
||||
action: getLaserAction(payload),
|
||||
});
|
||||
reply({ ignored: true, reason: 'roomLightsLockedOn' });
|
||||
return;
|
||||
}
|
||||
const isRebootCommand = type === 'reboot';
|
||||
const isUpdateCommand = type === 'update';
|
||||
const isSongCommand = type === 'song' || (type === 'raw' && isSongRawPayload(payload));
|
||||
@@ -235,6 +290,23 @@ io.on('connection', (socket) => {
|
||||
socket.on('command:issue', handleCommand);
|
||||
});
|
||||
|
||||
homeAssistantService.homeAssistantEvents.on('update', () => {
|
||||
const lockedOn = isRoomLightsLockedOn();
|
||||
if (lockedOn && !roomLightsWereLockedOn) {
|
||||
enforceLaserRoomLightLock('roomLightsLockedOn');
|
||||
}
|
||||
roomLightsWereLockedOn = lockedOn;
|
||||
});
|
||||
|
||||
roverManager.managerEvents.on('rover', (event = {}) => {
|
||||
// A rover can reconnect with laser.initialOn enabled or stale hardware state.
|
||||
// When room lights are locked on, every newly upserted rover gets an explicit
|
||||
// off command so the lock policy is true even across reconnects.
|
||||
if (event.action === 'upsert') {
|
||||
enforceLaserRoomLightLock('roverUpsertWhileRoomLightsLockedOn');
|
||||
}
|
||||
});
|
||||
|
||||
function isSongRawPayload(payload) {
|
||||
if (!payload) return false;
|
||||
const raw = payload.raw;
|
||||
|
||||
@@ -9,6 +9,7 @@ const neatoService = require('../neatoService');
|
||||
const liftService = require('../liftService');
|
||||
const {
|
||||
HEADLIGHT_DISABLE_ACTION,
|
||||
LASER_DISABLE_ACTION,
|
||||
DOCK_COMMAND_BASE64,
|
||||
} = require('./constants');
|
||||
|
||||
@@ -79,6 +80,29 @@ async function disableAllRoverHeadlights() {
|
||||
return { action: 'disableRoverHeadlights', attempted, failed };
|
||||
}
|
||||
|
||||
async function disableAllRoverLasers() {
|
||||
const attempted = [];
|
||||
const failed = [];
|
||||
roverManager.rovers.forEach((record) => {
|
||||
if (!record?.ws || !record?.meta?.laser?.enabled) return;
|
||||
const roverId = String(record.id);
|
||||
try {
|
||||
// Idle cleanup is allowed to send an explicit off command even when
|
||||
// other laser commands are policy-blocked. The point of this action is
|
||||
// to leave the rover in a non-emitting state when nobody is actively
|
||||
// driving it.
|
||||
issueCommand(roverId, {
|
||||
type: 'laser',
|
||||
laser: { action: LASER_DISABLE_ACTION },
|
||||
});
|
||||
attempted.push(roverId);
|
||||
} catch (err) {
|
||||
failed.push({ roverId, error: err.message });
|
||||
}
|
||||
});
|
||||
return { action: 'disableRoverLasers', attempted, failed };
|
||||
}
|
||||
|
||||
async function sendNeatoHome() {
|
||||
try {
|
||||
await neatoService.sendHome();
|
||||
@@ -101,6 +125,7 @@ const idleActions = [
|
||||
turnOffRoomControls,
|
||||
// dockAllRovers,
|
||||
disableAllRoverHeadlights,
|
||||
disableAllRoverLasers,
|
||||
sendNeatoHome,
|
||||
raiseLift,
|
||||
];
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
// Scope: Centralizes immutable configuration for trigger windows and rover command payloads.
|
||||
const IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
||||
const HEADLIGHT_DISABLE_ACTION = 'off';
|
||||
const LASER_DISABLE_ACTION = 'off';
|
||||
const DOCK_COMMAND_BASE64 = Buffer.from([143]).toString('base64');
|
||||
|
||||
module.exports = {
|
||||
IDLE_TIMEOUT_MS,
|
||||
HEADLIGHT_DISABLE_ACTION,
|
||||
LASER_DISABLE_ACTION,
|
||||
DOCK_COMMAND_BASE64,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user