mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
big loud turn indicators
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D-77ZqXZ.js"></script>
|
<script type="module" crossorigin src="/assets/index-CJBHX9So.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Cc3cNOzE.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D46FE2Sk.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSession } from '../context/SessionContext.jsx';
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||||
import { useVideoRequests } from '../hooks/useVideoRequests.js';
|
import { useVideoRequests } from '../hooks/useVideoRequests.js';
|
||||||
@@ -9,9 +9,12 @@ import VideoTile from './VideoTile.jsx';
|
|||||||
export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
const {
|
const {
|
||||||
state: { song },
|
state: { song, lastControlIntentAt },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
|
const [turnCueVisible, setTurnCueVisible] = useState(false);
|
||||||
|
const [turnCueStartAt, setTurnCueStartAt] = useState(null);
|
||||||
|
const lastTurnRef = useRef({ active: false, roverId: null });
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session?.mode !== 'turns') {
|
if (session?.mode !== 'turns') {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -38,10 +41,25 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
|||||||
}, [turnInfo?.queue, turnInfo?.current]);
|
}, [turnInfo?.queue, turnInfo?.current]);
|
||||||
const isNextDriver = Boolean(socketId && nextDriverId === socketId);
|
const isNextDriver = Boolean(socketId && nextDriverId === socketId);
|
||||||
const deadline = turnInfo?.deadline || null;
|
const deadline = turnInfo?.deadline || null;
|
||||||
|
const idleDeadline = turnInfo?.idleDeadline || null;
|
||||||
const msUntilTurn = deadline ? deadline - now : null;
|
const msUntilTurn = deadline ? deadline - now : null;
|
||||||
|
const msUntilIdleSkip = idleDeadline ? idleDeadline - now : null;
|
||||||
const isPreSwitchWindow =
|
const isPreSwitchWindow =
|
||||||
session?.mode === 'turns' && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0;
|
session?.mode === 'turns' && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0;
|
||||||
const shouldShowVideo = session?.mode !== 'turns' || isActiveDriver || isPreSwitchWindow;
|
const shouldShowVideo = session?.mode !== 'turns' || isActiveDriver || isPreSwitchWindow;
|
||||||
|
const turnSeconds =
|
||||||
|
msUntilTurn != null && Number.isFinite(msUntilTurn) ? Math.max(0, Math.ceil(msUntilTurn / 1000)) : null;
|
||||||
|
const idleSkipSeconds =
|
||||||
|
msUntilIdleSkip != null && Number.isFinite(msUntilIdleSkip)
|
||||||
|
? Math.max(0, Math.ceil(msUntilIdleSkip / 1000))
|
||||||
|
: null;
|
||||||
|
const turnTimerText = isActiveDriver
|
||||||
|
? turnSeconds != null
|
||||||
|
? `${turnSeconds}s left`
|
||||||
|
: null
|
||||||
|
: isNextDriver && turnSeconds != null
|
||||||
|
? `Your turn in ${turnSeconds}s`
|
||||||
|
: null;
|
||||||
const entries = roverId
|
const entries = roverId
|
||||||
? [
|
? [
|
||||||
...(shouldShowVideo ? [{ type: 'rover', id: roverId, key: roverId }] : []),
|
...(shouldShowVideo ? [{ type: 'rover', id: roverId, key: roverId }] : []),
|
||||||
@@ -65,6 +83,33 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
|||||||
|
|
||||||
const roverLabel = batteryRecord?.name || (roverId ? `Rover ${roverId}` : '');
|
const roverLabel = batteryRecord?.name || (roverId ? `Rover ${roverId}` : '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session?.mode !== 'turns') {
|
||||||
|
setTurnCueVisible(false);
|
||||||
|
setTurnCueStartAt(null);
|
||||||
|
lastTurnRef.current = { active: false, roverId: null };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastTurn = lastTurnRef.current;
|
||||||
|
const becameActive = isActiveDriver && !lastTurn.active;
|
||||||
|
const roverChanged = isActiveDriver && roverId && roverId !== lastTurn.roverId;
|
||||||
|
if (becameActive || roverChanged) {
|
||||||
|
setTurnCueVisible(true);
|
||||||
|
setTurnCueStartAt(Date.now());
|
||||||
|
} else if (!isActiveDriver && lastTurn.active) {
|
||||||
|
setTurnCueVisible(false);
|
||||||
|
setTurnCueStartAt(null);
|
||||||
|
}
|
||||||
|
lastTurnRef.current = { active: isActiveDriver, roverId };
|
||||||
|
}, [isActiveDriver, roverId, session?.mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!turnCueVisible || !turnCueStartAt) return;
|
||||||
|
if (lastControlIntentAt > turnCueStartAt) {
|
||||||
|
setTurnCueVisible(false);
|
||||||
|
}
|
||||||
|
}, [lastControlIntentAt, turnCueStartAt, turnCueVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
{roverId ? (
|
{roverId ? (
|
||||||
@@ -79,6 +124,11 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
|
|||||||
layoutFormat={layoutFormat}
|
layoutFormat={layoutFormat}
|
||||||
songNote={song?.note}
|
songNote={song?.note}
|
||||||
qualityNotice={!shouldShowVideo ? 'Preview feed (low FPS) until your turn.' : null}
|
qualityNotice={!shouldShowVideo ? 'Preview feed (low FPS) until your turn.' : null}
|
||||||
|
showTurnCue={turnCueVisible}
|
||||||
|
turnTimerText={turnTimerText}
|
||||||
|
turnSeconds={turnSeconds}
|
||||||
|
isActiveDriver={isActiveDriver}
|
||||||
|
idleSkipSeconds={idleSkipSeconds}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-video">
|
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-video">
|
||||||
|
|||||||
@@ -164,6 +164,14 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
|||||||
const queue = info?.queue || [];
|
const queue = info?.queue || [];
|
||||||
const deadline = info?.idleDeadline || info?.deadline || null;
|
const deadline = info?.idleDeadline || info?.deadline || null;
|
||||||
const remaining = secondsRemaining(deadline);
|
const remaining = secondsRemaining(deadline);
|
||||||
|
const currentId = info?.current || null;
|
||||||
|
const currentIdx = currentId ? queue.findIndex((id) => id === currentId) : -1;
|
||||||
|
const nextId =
|
||||||
|
queue.length > 1
|
||||||
|
? currentIdx >= 0
|
||||||
|
? queue[(currentIdx + 1) % queue.length]
|
||||||
|
: queue[0]
|
||||||
|
: null;
|
||||||
return (
|
return (
|
||||||
<div key={roverId} className="surface-muted flex flex-col gap-0.25 text-sm">
|
<div key={roverId} className="surface-muted flex flex-col gap-0.25 text-sm">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -180,21 +188,28 @@ export default function UserListPanel({ hideNicknameForm = false, hideHeader = f
|
|||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{queue.map((socketId, idx) => {
|
{queue.map((socketId, idx) => {
|
||||||
const user = lookupUser(socketId);
|
const user = lookupUser(socketId);
|
||||||
const isCurrent = socketId === info?.current;
|
const isCurrent = socketId === currentId;
|
||||||
|
const isNext = Boolean(nextId && socketId === nextId && !isCurrent);
|
||||||
|
const isSelf = Boolean(selfId && socketId === selfId);
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin';
|
user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin';
|
||||||
|
const highlightClass = isCurrent
|
||||||
|
? 'bg-sky-600 text-white ring-2 ring-amber-300 animate-pulse'
|
||||||
|
: isNext
|
||||||
|
? 'bg-emerald-700/60 text-emerald-100 ring-1 ring-emerald-300/70'
|
||||||
|
: 'bg-slate-800 text-slate-200';
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={`${roverId}-${socketId}-${idx}`}
|
key={`${roverId}-${socketId}-${idx}`}
|
||||||
className={`flex items-center gap-0.5 rounded px-1 text-[0.8rem] ${
|
className={`flex items-center gap-0.5 rounded px-1 text-[0.8rem] ${highlightClass}`}
|
||||||
isCurrent ? 'bg-sky-700 text-white' : 'bg-slate-800 text-slate-200'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className={`${roleColors(user.role)} font-semibold`}>
|
<span className={`${roleColors(user.role)} font-semibold`}>
|
||||||
{formatLabel(user, selfId)}
|
{formatLabel(user, selfId)}
|
||||||
</span>
|
</span>
|
||||||
{isAdmin && <span className="text-[0.7rem] text-amber-200">★</span>}
|
{isAdmin && <span className="text-[0.7rem] text-amber-200">★</span>}
|
||||||
|
{isSelf && <span className="text-[0.7rem] text-white">YOU</span>}
|
||||||
{isCurrent && <span className="text-[0.7rem] text-slate-200">now</span>}
|
{isCurrent && <span className="text-[0.7rem] text-slate-200">now</span>}
|
||||||
|
{isNext && <span className="text-[0.7rem] text-emerald-100">next</span>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export default function VideoTile({
|
|||||||
hudForceMap = false,
|
hudForceMap = false,
|
||||||
hudMapPosition = 'top-right',
|
hudMapPosition = 'top-right',
|
||||||
fitParent = false,
|
fitParent = false,
|
||||||
|
showTurnCue = false,
|
||||||
|
turnTimerText = null,
|
||||||
|
turnSeconds = null,
|
||||||
|
isActiveDriver = false,
|
||||||
|
idleSkipSeconds = null,
|
||||||
}) {
|
}) {
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
const audioRef = useRef(null);
|
const audioRef = useRef(null);
|
||||||
@@ -387,6 +392,14 @@ export default function VideoTile({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<audio ref={audioRef} autoPlay hidden />
|
<audio ref={audioRef} autoPlay hidden />
|
||||||
|
{showTurnCue ? (
|
||||||
|
<TurnCueOverlay
|
||||||
|
mobileHud={mobileHud}
|
||||||
|
turnSeconds={turnSeconds}
|
||||||
|
isActiveDriver={isActiveDriver}
|
||||||
|
idleSkipSeconds={idleSkipSeconds}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<HudOverlay
|
<HudOverlay
|
||||||
frame={telemetryFrame}
|
frame={telemetryFrame}
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -401,6 +414,7 @@ export default function VideoTile({
|
|||||||
showTopDown={showHudMap}
|
showTopDown={showHudMap}
|
||||||
mobileHud={mobileHud}
|
mobileHud={mobileHud}
|
||||||
mapPosition={hudMapPosition}
|
mapPosition={hudMapPosition}
|
||||||
|
turnTimerText={turnTimerText}
|
||||||
/>
|
/>
|
||||||
<HudChatInput compact={mobileHud} />
|
<HudChatInput compact={mobileHud} />
|
||||||
<OvercurrentOverlay motors={overcurrentMotors} compact={mobileHud} />
|
<OvercurrentOverlay motors={overcurrentMotors} compact={mobileHud} />
|
||||||
@@ -409,12 +423,14 @@ export default function VideoTile({
|
|||||||
<BatteryBarVertical visual={batteryVisual} />
|
<BatteryBarVertical visual={batteryVisual} />
|
||||||
) : null}
|
) : null}
|
||||||
{qualityNotice ? (
|
{qualityNotice ? (
|
||||||
<div
|
<div className="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2">
|
||||||
className={`pointer-events-none absolute rounded bg-black/70 font-semibold text-amber-200 ${
|
<div
|
||||||
mobileHud ? 'left-0.5 top-0.5 px-0.5 py-0.25 text-[0.55rem]' : 'left-1 top-1 px-1 py-0.5 text-xs'
|
className={`mx-auto w-fit rounded border border-amber-300/80 bg-black/75 text-amber-200 ${
|
||||||
}`}
|
mobileHud ? 'px-2 py-1 text-[0.6rem]' : 'px-3 py-1.5 text-sm'
|
||||||
>
|
}`}
|
||||||
{qualityNotice}
|
>
|
||||||
|
{qualityNotice}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -527,6 +543,7 @@ function HudOverlay({
|
|||||||
showTopDown = false,
|
showTopDown = false,
|
||||||
mobileHud = false,
|
mobileHud = false,
|
||||||
mapPosition = 'top-right',
|
mapPosition = 'top-right',
|
||||||
|
turnTimerText = null,
|
||||||
}) {
|
}) {
|
||||||
const bumps = sensors?.bumpsAndWheelDrops || {};
|
const bumps = sensors?.bumpsAndWheelDrops || {};
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
@@ -544,6 +561,8 @@ function HudOverlay({
|
|||||||
const labelPadClass = isMobile ? 'px-0.25 py-0.25' : 'px-0.5 py-0.5';
|
const labelPadClass = isMobile ? 'px-0.25 py-0.25' : 'px-0.5 py-0.5';
|
||||||
const labelTextClass = isMobile ? 'text-[0.55rem]' : 'text-[0.8rem]';
|
const labelTextClass = isMobile ? 'text-[0.55rem]' : 'text-[0.8rem]';
|
||||||
const statusPosClass = isMobile ? 'left-0.5 top-0.5' : 'left-1 top-1';
|
const statusPosClass = isMobile ? 'left-0.5 top-0.5' : 'left-1 top-1';
|
||||||
|
const timerTextClass = isMobile ? 'text-[0.5rem]' : 'text-[0.7rem]';
|
||||||
|
const timerPadClass = isMobile ? 'px-0.5 py-0.25' : 'px-1 py-0.5';
|
||||||
const telemetryPosClass = isMobile ? 'left-0.5 top-1/2' : 'left-1 top-1/2';
|
const telemetryPosClass = isMobile ? 'left-0.5 top-1/2' : 'left-1 top-1/2';
|
||||||
const labelPosClass = isMobile ? 'bottom-0.5' : 'bottom-0.5';
|
const labelPosClass = isMobile ? 'bottom-0.5' : 'bottom-0.5';
|
||||||
const mapSize = '240px';
|
const mapSize = '240px';
|
||||||
@@ -625,6 +644,13 @@ function HudOverlay({
|
|||||||
{audioStatus ? <span>Audio: {audioStatus}</span> : null}
|
{audioStatus ? <span>Audio: {audioStatus}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{turnTimerText ? (
|
||||||
|
<div
|
||||||
|
className={`absolute left-1/2 top-0.5 -translate-x-1/2 rounded bg-black/70 text-slate-100 ${timerPadClass} ${timerTextClass}`}
|
||||||
|
>
|
||||||
|
{turnTimerText}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={`absolute ${labelPosClass} left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}
|
className={`absolute ${labelPosClass} left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}
|
||||||
>
|
>
|
||||||
@@ -648,6 +674,32 @@ function HudOverlay({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TurnCueOverlay({
|
||||||
|
mobileHud = false,
|
||||||
|
turnSeconds = null,
|
||||||
|
isActiveDriver = false,
|
||||||
|
idleSkipSeconds = null,
|
||||||
|
}) {
|
||||||
|
const titleClass = mobileHud ? 'text-3xl' : 'text-5xl';
|
||||||
|
const subClass = mobileHud ? 'text-xs' : 'text-sm';
|
||||||
|
const timerClass = mobileHud ? 'text-[0.55rem]' : 'text-[0.75rem]';
|
||||||
|
const padClass = mobileHud ? 'px-4 py-3' : 'px-6 py-4';
|
||||||
|
const showCountdown = isActiveDriver && typeof idleSkipSeconds === 'number';
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center bg-black/55">
|
||||||
|
<div className={`flex flex-col items-center gap-1 rounded border border-amber-300/80 bg-black/70 ${padClass}`}>
|
||||||
|
<div className={`font-semibold text-amber-200 ${titleClass}`}>IT IS YOUR TURN!</div>
|
||||||
|
<div className={`text-amber-200/80 ${subClass}`}>Start driving!</div>
|
||||||
|
{showCountdown ? (
|
||||||
|
<div className={`text-red-100/90 ${timerClass}`}>
|
||||||
|
Idle skip in {idleSkipSeconds}s
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const OVERCURRENT_LABELS = {
|
const OVERCURRENT_LABELS = {
|
||||||
leftWheel: 'Left wheel',
|
leftWheel: 'Left wheel',
|
||||||
rightWheel: 'Right wheel',
|
rightWheel: 'Right wheel',
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ export function ControlSystemProvider({ children }) {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const recordControlIntent = useCallback(() => {
|
||||||
|
dispatch({ type: 'control/record-intent' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setDriveVector = useCallback(
|
const setDriveVector = useCallback(
|
||||||
(vector, meta = {}) => {
|
(vector, meta = {}) => {
|
||||||
const computed = computeDifferentialSpeeds(vector, meta.speedOptions);
|
const computed = computeDifferentialSpeeds(vector, meta.speedOptions);
|
||||||
@@ -127,9 +131,10 @@ export function ControlSystemProvider({ children }) {
|
|||||||
type: 'control/update-drive',
|
type: 'control/update-drive',
|
||||||
payload: { ...computed, source: meta.source ?? null },
|
payload: { ...computed, source: meta.source ?? null },
|
||||||
});
|
});
|
||||||
|
recordControlIntent();
|
||||||
pipeline.sendDriveDirect(computed.speeds);
|
pipeline.sendDriveDirect(computed.speeds);
|
||||||
},
|
},
|
||||||
[pipeline],
|
[pipeline, recordControlIntent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setAuxMotors = useCallback(
|
const setAuxMotors = useCallback(
|
||||||
@@ -139,8 +144,9 @@ export function ControlSystemProvider({ children }) {
|
|||||||
type: 'control/set-aux-motors',
|
type: 'control/set-aux-motors',
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
recordControlIntent();
|
||||||
},
|
},
|
||||||
[pipeline],
|
[pipeline, recordControlIntent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateKeyBinding = useCallback(
|
const updateKeyBinding = useCallback(
|
||||||
@@ -176,8 +182,9 @@ export function ControlSystemProvider({ children }) {
|
|||||||
dispatch({ type: 'control/set-camera-angle', payload: clamped });
|
dispatch({ type: 'control/set-camera-angle', payload: clamped });
|
||||||
pipeline.sendServoAngle(clamped);
|
pipeline.sendServoAngle(clamped);
|
||||||
servoAngleRef.current = clamped;
|
servoAngleRef.current = clamped;
|
||||||
|
recordControlIntent();
|
||||||
},
|
},
|
||||||
[pipeline],
|
[pipeline, recordControlIntent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const nudgeServo = useCallback(
|
const nudgeServo = useCallback(
|
||||||
@@ -217,10 +224,13 @@ export function ControlSystemProvider({ children }) {
|
|||||||
if (!session?.homeAssistant?.entities) {
|
if (!session?.homeAssistant?.entities) {
|
||||||
pendingLightsRef.current = true;
|
pendingLightsRef.current = true;
|
||||||
}
|
}
|
||||||
|
recordControlIntent();
|
||||||
|
} else if (macroId === 'seek-dock') {
|
||||||
|
recordControlIntent();
|
||||||
}
|
}
|
||||||
await pipeline.runMacroSteps(macro);
|
await pipeline.runMacroSteps(macro);
|
||||||
},
|
},
|
||||||
[pipeline, state.macros, session?.homeAssistant?.entities, turnOnAllLights],
|
[pipeline, recordControlIntent, session?.homeAssistant?.entities, state.macros, turnOnAllLights],
|
||||||
);
|
);
|
||||||
|
|
||||||
const stopAllMotion = useCallback(() => {
|
const stopAllMotion = useCallback(() => {
|
||||||
@@ -256,7 +266,8 @@ export function ControlSystemProvider({ children }) {
|
|||||||
|
|
||||||
const toggleNightVision = useCallback(() => {
|
const toggleNightVision = useCallback(() => {
|
||||||
pipeline.sendNightVision('toggle');
|
pipeline.sendNightVision('toggle');
|
||||||
}, [pipeline]);
|
recordControlIntent();
|
||||||
|
}, [pipeline, recordControlIntent]);
|
||||||
|
|
||||||
const setSongNote = useCallback(
|
const setSongNote = useCallback(
|
||||||
(note) => {
|
(note) => {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const initialControlState = {
|
|||||||
aux: createAuxState(),
|
aux: createAuxState(),
|
||||||
camera: createCameraState(),
|
camera: createCameraState(),
|
||||||
song: createSongState(),
|
song: createSongState(),
|
||||||
|
lastControlIntentAt: 0,
|
||||||
macros: DEFAULT_MACROS,
|
macros: DEFAULT_MACROS,
|
||||||
keymap: DEFAULT_KEYMAP,
|
keymap: DEFAULT_KEYMAP,
|
||||||
inputs: {},
|
inputs: {},
|
||||||
@@ -48,6 +49,7 @@ export function controlReducer(state, action) {
|
|||||||
drive: action.payload ? state.drive : createDriveState(),
|
drive: action.payload ? state.drive : createDriveState(),
|
||||||
aux: action.payload ? state.aux : createAuxState(),
|
aux: action.payload ? state.aux : createAuxState(),
|
||||||
song: action.payload ? state.song : createSongState(),
|
song: action.payload ? state.song : createSongState(),
|
||||||
|
lastControlIntentAt: action.payload ? state.lastControlIntentAt : 0,
|
||||||
};
|
};
|
||||||
case 'control/set-mode':
|
case 'control/set-mode':
|
||||||
return state.mode === action.payload
|
return state.mode === action.payload
|
||||||
@@ -123,6 +125,12 @@ export function controlReducer(state, action) {
|
|||||||
drive: createDriveState(),
|
drive: createDriveState(),
|
||||||
aux: createAuxState(),
|
aux: createAuxState(),
|
||||||
song: createSongState(),
|
song: createSongState(),
|
||||||
|
lastControlIntentAt: 0,
|
||||||
|
};
|
||||||
|
case 'control/record-intent':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
lastControlIntentAt: Date.now(),
|
||||||
};
|
};
|
||||||
case 'control/set-song-note':
|
case 'control/set-song-note':
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user