mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 10:00:46 -04:00
spotlite
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
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/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>
|
<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>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-C_Ron8xr.js"></script>
|
<script type="module" crossorigin src="/assets/index-CWTcRRlU.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DwpUkSbG.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DwpUkSbG.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const DEFAULT_REPLAY_ENABLED = false;
|
|||||||
const SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
|
const SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
|
||||||
const SNAPSHOT_POLL_MS = 300;
|
const SNAPSHOT_POLL_MS = 300;
|
||||||
const SNAPSHOT_STREAM_INTERVAL_MS = 2000;
|
const SNAPSHOT_STREAM_INTERVAL_MS = 2000;
|
||||||
|
const SPOTLIGHT_VERIFY_DELAY_MS = 1200;
|
||||||
|
|
||||||
const events = new EventEmitter();
|
const events = new EventEmitter();
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@@ -59,6 +60,7 @@ let blockedTimer = null;
|
|||||||
let publisherProcess = null;
|
let publisherProcess = null;
|
||||||
let publisherRestartTimer = null;
|
let publisherRestartTimer = null;
|
||||||
let snapshotTimer = null;
|
let snapshotTimer = null;
|
||||||
|
let spotlightVerifyTimer = null;
|
||||||
let vendorStatePromise = Promise.resolve();
|
let vendorStatePromise = Promise.resolve();
|
||||||
let lastSnapshotState = null;
|
let lastSnapshotState = null;
|
||||||
const snapshotSubscribers = new Map();
|
const snapshotSubscribers = new Map();
|
||||||
@@ -109,6 +111,20 @@ function normalizeSpotlightState(light) {
|
|||||||
return { ...light, on: isSpotlightOn(light) };
|
return { ...light, on: isSpotlightOn(light) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSpotlightPayloadState(rawState) {
|
||||||
|
/*
|
||||||
|
Socket payloads can arrive as booleans, numbers, or strings depending on
|
||||||
|
which control path produced them. Boolean("0") is true in JavaScript, so do
|
||||||
|
an explicit conversion here before building the Reolink payload.
|
||||||
|
*/
|
||||||
|
if (typeof rawState === 'string') {
|
||||||
|
const normalized = rawState.trim().toLowerCase();
|
||||||
|
if (['1', 'on', 'true', 'yes'].includes(normalized)) return true;
|
||||||
|
if (['0', 'off', 'false', 'no'].includes(normalized)) return false;
|
||||||
|
}
|
||||||
|
return Boolean(Number(rawState));
|
||||||
|
}
|
||||||
|
|
||||||
function passesMode(socket) {
|
function passesMode(socket) {
|
||||||
const mode = getMode();
|
const mode = getMode();
|
||||||
if (mode === MODES.LOCKDOWN) return isLockdownAdmin(socket);
|
if (mode === MODES.LOCKDOWN) return isLockdownAdmin(socket);
|
||||||
@@ -354,6 +370,34 @@ async function refreshVendorState() {
|
|||||||
state.ir = ir?.IrLights || ir || null;
|
state.ir = ir?.IrLights || ir || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshSpotlightState() {
|
||||||
|
const client = await ensureReolinkClient();
|
||||||
|
const white = await client.api('GetWhiteLed', { channel: 0 });
|
||||||
|
state.light = normalizeSpotlightState(white?.WhiteLed || white || null);
|
||||||
|
emitChange('light');
|
||||||
|
return state.light;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSpotlightVerification() {
|
||||||
|
/*
|
||||||
|
This camera acknowledges SetWhiteLed before GetWhiteLed catches up. A read
|
||||||
|
immediately after a successful write returns the old value for roughly one
|
||||||
|
second, which made the UI appear inverted or flaky. Replace any pending
|
||||||
|
verification with one delayed read so rapid toggles settle on the newest
|
||||||
|
requested state instead of racing stale camera state back into the session.
|
||||||
|
*/
|
||||||
|
if (spotlightVerifyTimer) {
|
||||||
|
clearTimeout(spotlightVerifyTimer);
|
||||||
|
spotlightVerifyTimer = null;
|
||||||
|
}
|
||||||
|
spotlightVerifyTimer = setTimeout(() => {
|
||||||
|
spotlightVerifyTimer = null;
|
||||||
|
serializeVendorState(() => refreshSpotlightState()).catch((err) => {
|
||||||
|
logger.warn('spotlight verification failed', { error: err.message });
|
||||||
|
});
|
||||||
|
}, SPOTLIGHT_VERIFY_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
function serializeVendorState(operation) {
|
function serializeVendorState(operation) {
|
||||||
/*
|
/*
|
||||||
The Reolink HTTP API can return stale light state when reads and writes are
|
The Reolink HTTP API can return stale light state when reads and writes are
|
||||||
@@ -562,27 +606,43 @@ async function setSpotlight(socket, payload = {}) {
|
|||||||
requireOperator(socket);
|
requireOperator(socket);
|
||||||
return serializeVendorState(async () => {
|
return serializeVendorState(async () => {
|
||||||
const client = await ensureReolinkClient();
|
const client = await ensureReolinkClient();
|
||||||
const current = normalizeSpotlightState((await client.api('GetWhiteLed', { channel: 0 })).WhiteLed || {});
|
let current = state.light ? normalizeSpotlightState(state.light) : null;
|
||||||
const logicalOn = payload.state === undefined ? !current.on : Boolean(payload.state);
|
if (payload.state === undefined && !current) {
|
||||||
const next = {
|
/*
|
||||||
...current,
|
Toggle requests need a base state. Normal button paths send an explicit
|
||||||
|
state, so this read only happens for rare generic toggle callers or
|
||||||
|
startup races before the initial vendor state has arrived.
|
||||||
|
*/
|
||||||
|
current = await refreshSpotlightState();
|
||||||
|
}
|
||||||
|
const logicalOn = payload.state === undefined
|
||||||
|
? !isSpotlightOn(current || {})
|
||||||
|
: normalizeSpotlightPayloadState(payload.state);
|
||||||
|
const cameraState = spotlightCameraStateForLogicalOn(logicalOn);
|
||||||
|
const cameraPayload = {
|
||||||
channel: 0,
|
channel: 0,
|
||||||
state: spotlightCameraStateForLogicalOn(logicalOn),
|
state: cameraState,
|
||||||
|
};
|
||||||
|
const next = {
|
||||||
|
...(current || {}),
|
||||||
|
...cameraPayload,
|
||||||
on: logicalOn,
|
on: logicalOn,
|
||||||
};
|
};
|
||||||
if (Number.isFinite(Number(payload.bright))) {
|
if (Number.isFinite(Number(payload.bright))) {
|
||||||
next.bright = Math.max(0, Math.min(100, Number(payload.bright)));
|
const bright = Math.max(0, Math.min(100, Number(payload.bright)));
|
||||||
|
cameraPayload.bright = bright;
|
||||||
|
next.bright = bright;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Update local state optimistically before the refresh. That makes the
|
The camera accepts a minimal WhiteLed payload and reports success before
|
||||||
headlight button feel deterministic while the follow-up read still lets
|
GetWhiteLed reflects the new state. Send only the fields we intend to
|
||||||
the camera correct anything it refused or normalized.
|
change, then keep the optimistic state until the delayed verification read
|
||||||
|
has a real chance to observe the update.
|
||||||
*/
|
*/
|
||||||
state.light = next;
|
state.light = next;
|
||||||
emitChange('light-pending');
|
emitChange('light-pending');
|
||||||
await client.api('SetWhiteLed', { WhiteLed: next });
|
await client.api('SetWhiteLed', { WhiteLed: cameraPayload });
|
||||||
await refreshVendorState();
|
scheduleSpotlightVerification();
|
||||||
emitChange('light');
|
|
||||||
return state.light;
|
return state.light;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useSessionSelector } from '../context/SessionContext.jsx';
|
|||||||
|
|
||||||
const PTZ_STOP = { pan: 0, tilt: 0, zoom: 0 };
|
const PTZ_STOP = { pan: 0, tilt: 0, zoom: 0 };
|
||||||
const PTZ_SPEEDS = {
|
const PTZ_SPEEDS = {
|
||||||
slow: 0.15,
|
slow: 0.7,
|
||||||
medium: 0.5,
|
medium: 0.5,
|
||||||
fast: 1,
|
fast: 1,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user