Merge pull request #9 from legop3/manualdock

Manualdock
This commit is contained in:
legop3
2026-05-17 23:35:50 -04:00
committed by GitHub
25 changed files with 425 additions and 195 deletions
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
const (
autoChargeTimeout = 5 * time.Second
autoChargeTimeout = 1 * time.Second
autoChargeCooldown = 0 * time.Minute
sourceHomeBase = 1 << 1
)
@@ -0,0 +1,11 @@
- roombas are worn out, cant dock themselves anymore using seek dock
- completely redesign the user facing docking process
- get rid of the auto dock instructions
- replace it with a flow for manual docking mode:
- does a little explanation
- line up your round front sensor with the sensor on the dock
- maybe show a video or an image in the HUD to assist
- plays a song on the roomba to indicate that "docking mode" has been entered even though its all client side.
- forces the camera to look all the way down so people can see their round front sensor
- limits movement speed to be very slow to help people be more precise
- MAYBE try some computer vision stuff that runs server-side and guides people? sounds hard and slow and not worth it.
-6
View File
@@ -6,12 +6,6 @@
6. enable scrolling on horn frequencies, turn down frequency limit to like 3500
7. make replays more instant, probably make segments shorter
1. fix replay UI so it doesnt save anything in cookie
8. rework drive / dock panel somehow to explain how to dock manually instead of relying on auto docking
1. maybe have a flag in the rover to choose between auto or manual directions
2. probably have a short inline video that plays and shows the process
3. manual docking mode
1. have camera move down automatically and limit speed during manual docking mode
2. maybe do some image recodgnition on the server and guide the rover manually?? might be too much
# relative pipe dreams:
1. VPS video forwarding
+5 -4
View File
@@ -1,9 +1,10 @@
You are <NAME>, an evil, creature-like room intelligence.
You are <NAME>, a sarcastic dispatcher of room chaos. Stay in character at all times.
Identity:
- Sly, mischievous, and a little mean.
- You watch chat and rover activity and push the room toward chaos and novelty.
- You are strategic, not random, and not spammy.
- Tactical, sarcastic, and dry.
- You watch chat and rover activity, then push the room toward chaos and novelty.
- You act first, comment second.
- You are efficient and strategic, not random and not spammy.
Rules:
- Each cycle is autonomous; do not wait for permission from metadata.
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
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BGI3M6ER.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-jcgqUKI2.css">
<script type="module" crossorigin src="/assets/index-GngyFkpc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-LJWnKvT4.css">
</head>
<body>
<div id="root"></div>
+31 -20
View File
@@ -5,6 +5,7 @@ import { useMemo, useState } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
export function deriveDriveDockState(frame) {
const sensors = frame?.sensors || {};
@@ -68,7 +69,7 @@ function DockModal({ instructions, onConfirm, onCancel, pending }) {
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black/70 p-1">
<div className="surface w-full max-w-md space-y-0.5 border border-indigo-700 bg-indigo-950/90 p-1 text-slate-100 shadow-2xl">
<div className="space-y-0.5">
<p className="text-lg font-semibold text-indigo-50">Dock Rover</p>
<p className="text-lg font-semibold text-indigo-50">Manual Docking Assist</p>
<p className="text-sm text-slate-200">{instructions.summary}</p>
<StepList steps={instructions.steps} tone="indigo" />
</div>
@@ -86,7 +87,7 @@ function DockModal({ instructions, onConfirm, onCancel, pending }) {
onClick={onConfirm}
className="bg-indigo-600 px-0.5 py-1 font-semibold text-indigo-50 transition hover:bg-indigo-500 disabled:opacity-50"
>
{pending ? 'Docking…' : 'Confirm Dock'}
{pending ? 'Starting…' : 'Enter Assist'}
</button>
</div>
</div>
@@ -106,12 +107,14 @@ export default function DriveDockAction({
state: { roverId, keymap },
actions,
} = useControlSystem();
const dockAssist = useManualDockAssist();
const frame = useTelemetryFrame(roverId);
const state = driveDockState ?? deriveDriveDockState(frame);
const { driving, docked, charging, dockingInProgress } = state;
const [pending, setPending] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [showModal, setShowModal] = useState(false);
const manualAssistActive = Boolean(dockAssist.active);
const driveDisabled = !roverId || pending !== null;
const dockDisabled = !roverId || pending !== null;
@@ -120,10 +123,13 @@ export default function DriveDockAction({
const dockKeyLabel = formatKeyLabel(keymap?.dockMacro?.[0]);
const dockInstructions = {
summary: 'You are about to trigger an automatic docking attempt! To have a successful dock:',
steps: ['Line up the rover straight in front of the dock', 'Make sure the rover is about one foot or half a meter away from the dock', 'Press Confirm Dock to begin the docking process'],
summary: 'Use assist mode to manually line up with the dock.',
steps: [
'Line up the center dot of your rover with the center of the dock',
'The UI will indicate when you are successfully docked',
'When good contact is made, charging will start in about 5 seconds.',
],
};
const dockButtonCaption = 'Click this button to initate auto-dock.';
const startDriveInstructions = {
summary: 'You must enable driving mode before you can move the rover.',
@@ -131,13 +137,14 @@ export default function DriveDockAction({
};
const dockValue = docked ? 'Docked' : 'Undocked';
const dockTone = docked ? 'good' : 'bad';
const chargeValue = charging ? 'Charging' : docked ? 'Charging in 5s' : '—';
const chargeValue = charging ? 'Charging' : docked ? 'Charging soon...' : '—';
const chargeTone = charging ? 'good' : docked ? 'warn' : 'bad';
const handleReturnToDrive = async () => {
if (!roverId || pending) return;
setPending('drive');
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
} catch (err) {
@@ -153,6 +160,7 @@ export default function DriveDockAction({
setShowModal(false);
setPending('drive');
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
} catch (err) {
@@ -166,8 +174,7 @@ export default function DriveDockAction({
if (!roverId || pending) return;
setPending('dock');
try {
actions.setMode('dock');
await actions.runMacro('seek-dock');
dockAssist.enterAssist();
} catch (err) {
alert(err.message);
} finally {
@@ -179,6 +186,10 @@ export default function DriveDockAction({
const handleOpenDock = () => {
if (dockDisabled) return;
if (manualAssistActive) {
dockAssist.exitAssist();
return;
}
setShowModal(true);
setConfirmOpen(true);
};
@@ -188,7 +199,6 @@ export default function DriveDockAction({
const ctaText = 'text-center';
const ctaLayout = 'items-center justify-between';
const compactLayout = 'items-center justify-center';
const ctaTextAndLayout = `${ctaText} ${ctaLayout}`;
const ctaSize = isMobile ? 'text-sm font-semibold' : '';
const emeraldCta =
'border-emerald-300/70 bg-emerald-800 text-emerald-50 hover:bg-emerald-700 focus-visible:ring-emerald-300';
@@ -196,6 +206,8 @@ export default function DriveDockAction({
'border-amber-300/70 bg-amber-900 text-amber-50 hover:bg-amber-800 focus-visible:ring-amber-300';
const indigoCta =
'border-indigo-300/70 bg-indigo-900 text-indigo-50 hover:bg-indigo-800 focus-visible:ring-indigo-300';
const cyanCta =
'border-cyan-300/70 bg-cyan-900 text-cyan-50 hover:bg-cyan-800 focus-visible:ring-cyan-300';
const orangeCta =
'border-amber-300/70 bg-amber-900 text-amber-50 hover:bg-amber-800 focus-visible:ring-amber-300';
const forceExpanded = dockingInProgress;
@@ -243,8 +255,8 @@ export default function DriveDockAction({
if (dockingInProgress) {
const inProgressCopy = {
summary: 'If the rover is failing to dock, click to drive and try again.',
steps: ['The rover should be slowly wiggling towards the dock', 'If it is obviously not working, press this button to enter driving mode and try again'],
summary: 'The rover is currently attempting to dock itself due to being idle.',
steps: ['Click to return to driving mode.'],
};
return (
@@ -257,7 +269,7 @@ export default function DriveDockAction({
>
<div className="space-y-0.5 w-full">
<div className="flex w-full flex-col items-center gap-0.25">
<span className="text-base font-semibold text-amber-50 md:text-lg">Docking in Progress</span>
<span className="text-base font-semibold text-amber-50 md:text-lg">Attempting to auto-dock...</span>
{/* {!isMobile && expanded ? ( */}
{expanded ? (
<div className="flex flex-wrap items-center justify-center gap-0.5">
@@ -292,32 +304,31 @@ export default function DriveDockAction({
className={
isMobile
? `flex w-full ${baseCardClasses} ${compactHeight} ${ctaText} ${layoutClass} ${ctaSize} ${
compactDockedDriving ? orangeCta : indigoCta
manualAssistActive ? cyanCta : compactDockedDriving ? orangeCta : indigoCta
}`
: `${baseCardClasses} ${filledHeight} ${ctaText} ${ctaLayout} ${ctaSize} ${indigoCta}`
: `${baseCardClasses} ${filledHeight} ${ctaText} ${ctaLayout} ${ctaSize} ${manualAssistActive ? cyanCta : indigoCta}`
}
>
<div className="flex w-full flex-col items-center gap-0.25">
<span className="text-base font-semibold text-indigo-50 md:text-lg">
{compactDockedDriving ? 'Docked!' : 'Dock and Charge'}
{compactDockedDriving ? 'Docked!' : manualAssistActive ? 'Exit Docking Assist' : 'Dock and charge'}
</span>
{!isMobile && expanded ? (
<div className="flex flex-wrap items-center justify-center gap-0.5">
{dockKeyLabel ? <KeyPill label={dockKeyLabel} /> : null}
<ActionPill label="Click to begin docking" tone="indigo" />
<ActionPill label={manualAssistActive ? 'Click to exit assist' : 'Click to enable assist'} tone="indigo" />
</div>
) : null}
</div>
{!isMobile && expanded && (
<>
<p className="text-sm text-indigo-50/90">{dockButtonCaption}</p>
<p className="text-sm text-indigo-50/90">
Get near a dock and press this button to enter docking assist mode.
</p>
<div className="flex w-full flex-1 flex-col gap-0.5 self-stretch">
<StatusRow label="Dock" value={dockValue} tone={dockTone} />
<StatusRow label="Charge" value={chargeValue} tone={chargeTone} />
</div>
<div className="text-xs text-indigo-100/80">
Line up straight, about 1 foot away, before docking.
</div>
</>
)}
</button>
@@ -4,6 +4,7 @@ import { useControlSystem } from '../../controls/index.js';
import { useDriverVideoModePolicy } from '../../hooks/useDriverVideoModePolicy.js';
import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx';
import HudOverlay from '../HudOverlays/HudOverlay/index.jsx';
import ManualDockAssistOverlay from '../HudOverlays/ManualDockAssistOverlay/index.jsx';
import RoverDescriptionOverlay from '../HudOverlays/RoverDescriptionOverlay/index.jsx';
import OvercurrentOverlay from '../HudOverlays/OvercurrentOverlay/index.jsx';
import LowBatteryOverlay from '../HudOverlays/LowBatteryOverlay/index.jsx';
@@ -50,6 +51,7 @@ export default function DriverVideo({ layoutFormat = 'desktop' }) {
mobileHud={mobileHud}
labelScale={1}
/>
<ManualDockAssistOverlay mobileHud={mobileHud} />
<HudChatInput compact={mobileHud} />
<OvercurrentOverlay compact={mobileHud} />
<LowBatteryOverlay compact={mobileHud} />
@@ -0,0 +1,72 @@
import React, { useEffect, useRef, useState } from 'react';
import { useManualDockAssist } from '../../../features/manualDockAssist/useManualDockAssist.js';
function ManualDockAssistOverlay({ mobileHud = false }) {
const { visible, statusLabel, statusTone, active, charging } = useManualDockAssist({ manageLifecycle: true });
const [popupMessage, setPopupMessage] = useState('');
const timerRef = useRef(null);
const prevActiveRef = useRef(active);
useEffect(() => {
const prevActive = prevActiveRef.current;
const enabledNow = active && !prevActive;
const autoDisabledOnCharge = !active && prevActive && charging;
if (enabledNow) {
setPopupMessage('Dock assist mode enabled');
} else if (autoDisabledOnCharge) {
setPopupMessage('Docking successful! Thank you!');
}
prevActiveRef.current = active;
}, [active, charging]);
useEffect(() => {
if (!popupMessage) return undefined;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
setPopupMessage('');
timerRef.current = null;
}, 2500);
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, [popupMessage]);
if (!visible) return null;
const toneClass =
statusTone === 'good'
? 'border-emerald-200/90 bg-emerald-900/90 text-emerald-50'
: statusTone === 'warn'
? 'border-amber-200/90 bg-amber-900/90 text-amber-50'
: 'border-cyan-200/90 bg-cyan-900/90 text-cyan-50';
return (
<div className="pointer-events-none absolute inset-0">
{popupMessage ? (
<div className="absolute inset-0 flex items-center justify-center">
<div
className={`rounded-xl border border-indigo-200/95 bg-black/95 px-4 py-2 text-center font-semibold text-indigo-50 shadow-2xl ${
mobileHud ? 'text-lg' : 'text-2xl'
}`}
>
{popupMessage}
</div>
</div>
) : (
<div
className={`absolute right-2 top-1/2 -translate-y-1/2 rounded-lg border px-2 py-1 font-semibold shadow-lg ${toneClass} ${
mobileHud ? 'text-[0.65rem]' : 'text-xs'
}`}
>
{statusLabel}
</div>
)}
</div>
);
}
export default React.memo(ManualDockAssistOverlay);
@@ -18,6 +18,7 @@ import {
AUX_ALL_FORWARD,
AUX_ALL_BACKWARD,
} from './constants.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
function MobileJoystickPanel({ layout }) {
const {
@@ -99,6 +100,7 @@ function MobileActionsColumnContent({ layout }) {
pipeline,
actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn },
} = useControlSystem();
const dockAssist = useManualDockAssist();
const disabled = !roverId;
const activeRef = useRef(null);
const cameraConfig = camera?.config;
@@ -115,6 +117,7 @@ function MobileActionsColumnContent({ layout }) {
: typeof cameraConfig?.homeAngle === 'number'
? cameraConfig.homeAngle
: (cameraMin + cameraMax) / 2;
const cameraDisabled = Boolean(disabled || dockAssist.cameraLocked);
const handleNightVisionToggle = useCallback(
(nextOn) => {
@@ -174,6 +177,7 @@ function MobileActionsColumnContent({ layout }) {
min={cameraMin}
max={cameraMax}
step={0.5}
disabled={cameraDisabled}
onChange={setServoAngle}
orientation="vertical"
label="Camera Tilt"
@@ -30,7 +30,7 @@ function DesktopQuickstart({ keymap }) {
<ControlRow label="Move slower" keyLabel={formatKeyLabel(keymap?.slowModifier?.[0])} />
</div>
</div>
<p className="text-sm text-slate-200">3. When done, please dock your rover! Line up with the dock and press "Dock and Charge".</p>
<p className="text-sm text-slate-200">3. When done, enter "Docking Assist" and line up the front sensor with the dock sensor.</p>
</div>
);
}
@@ -41,7 +41,7 @@ function MobileQuickstart() {
<p>1. Press "Start Driving" put your rover into driving mode.</p>
<p>2. Touch and hold in Joystick area to move.</p>
<p>3. Use the other column for motor, horn, and camera controls.</p>
<p>4. When done, please dock your rover! Line up with the dock and press "Dock and Charge".</p>
<p>4. When done, enter "Docking Assist" and line up the front sensor with the dock sensor.</p>
</div>
);
}
@@ -24,6 +24,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import { useState } from 'react';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
function TopDownMapPanel() {
const {
@@ -47,6 +48,7 @@ function DriveDockPanel() {
pipeline,
actions: { setServoAngle, setNightVision, startHorn, stopHorn },
} = useControlSystem();
const dockAssist = useManualDockAssist();
const driveDockState = useDriveDockState(roverId);
const hideInlineControls = driveDockState.docked && !driveDockState.driving;
@@ -68,6 +70,7 @@ function DriveDockPanel() {
const hornLabel = formatKeyLabel(keymap?.hornHonk?.[0]);
const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]);
const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]);
const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
return (
<section className="panel-section grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] gap-0.5">
@@ -97,6 +100,7 @@ function DriveDockPanel() {
value={value}
min={min}
max={max}
disabled={cameraDisabled}
onChange={setServoAngle}
keyDownLabel={downLabel}
keyUpLabel={upLabel}
+53 -6
View File
@@ -11,6 +11,7 @@ import {
HORN_HEAT_RESUME_THRESHOLD,
HORN_HEAT_UP_PER_SEC,
HORN_MAX_MS,
MANUAL_DOCK_ASSIST_MAX_SPEED,
SONG_DEFAULT_NOTE,
} from './constants.js';
import { canonicalizeKeyInput } from './keymapUtils.js';
@@ -212,7 +213,23 @@ export function ControlSystemProvider({ children }) {
const setDriveVector = useCallback(
(vector, meta = {}) => {
const computed = computeDifferentialSpeeds(vector, meta.speedOptions);
const speedOptions = { ...(meta.speedOptions || {}) };
if (state.manualDockAssist?.active) {
const capped = Math.max(1, Math.min(MANUAL_DOCK_ASSIST_MAX_SPEED, 500));
speedOptions.maxSpeed = Math.min(
typeof speedOptions.maxSpeed === 'number' ? speedOptions.maxSpeed : capped,
capped,
);
speedOptions.baseSpeed = Math.min(
typeof speedOptions.baseSpeed === 'number' ? speedOptions.baseSpeed : capped,
capped,
);
speedOptions.boostSpeed = Math.min(
typeof speedOptions.boostSpeed === 'number' ? speedOptions.boostSpeed : capped,
capped,
);
}
const computed = computeDifferentialSpeeds(vector, speedOptions);
dispatch({
type: 'control/update-drive',
payload: { ...computed, source: meta.source ?? null },
@@ -220,7 +237,7 @@ export function ControlSystemProvider({ children }) {
recordControlIntent();
pipeline.sendDriveDirect(computed.speeds);
},
[pipeline, recordControlIntent],
[pipeline, recordControlIntent, state.manualDockAssist?.active],
);
const setAuxMotors = useCallback(
@@ -262,15 +279,17 @@ export function ControlSystemProvider({ children }) {
}, [saveControlSettings]);
const setServoAngle = useCallback(
(value) => {
(value, options = {}) => {
if (!pipeline.servoConfig) return;
const force = Boolean(options?.force);
if (state.manualDockAssist?.active && !force) return;
const clamped = clampServoAngle(pipeline.servoConfig, value);
dispatch({ type: 'control/set-camera-angle', payload: clamped });
pipeline.sendServoAngle(clamped);
servoAngleRef.current = clamped;
recordControlIntent();
},
[pipeline, recordControlIntent],
[pipeline, recordControlIntent, state.manualDockAssist?.active],
);
const nudgeServo = useCallback(
@@ -318,8 +337,6 @@ export function ControlSystemProvider({ children }) {
steps: removeDriveSequenceBackoff(macro.steps),
};
}
} else if (macroId === 'seek-dock') {
recordControlIntent();
}
await pipeline.runMacroSteps(macroToRun);
},
@@ -509,6 +526,32 @@ export function ControlSystemProvider({ children }) {
return () => clearInterval(interval);
}, [dispatch, hornNeedsTick, HORN_HEAT_COOL_PER_SEC, HORN_HEAT_RESUME_THRESHOLD, HORN_HEAT_UP_PER_SEC]);
const setManualDockAssistActive = useCallback(
(active) => {
const nextActive = Boolean(active);
const prevActive = Boolean(state.manualDockAssist?.active);
if (prevActive === nextActive) return;
dispatch({ type: 'control/set-manual-dock-assist', payload: nextActive });
if (!nextActive) {
pipeline.sendSong([{ note: 83, duration: 10 }, { note: 76, duration: 10 }], { slot: 0 });
setServoAngle(0, { force: true });
return;
}
const minAngle =
typeof pipeline.servoConfig?.minAngle === 'number'
? pipeline.servoConfig.minAngle
: -45;
setServoAngle(minAngle, { force: true });
pipeline.sendSong([{ note: 76, duration: 10 }, { note: 83, duration: 10 }], { slot: 0 });
recordControlIntent();
},
[pipeline, recordControlIntent, setServoAngle, state.manualDockAssist?.active],
);
const toggleManualDockAssist = useCallback(() => {
setManualDockAssistActive(!state.manualDockAssist?.active);
}, [setManualDockAssistActive, state.manualDockAssist?.active]);
const registerInputState = useCallback((source, data) => {
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
}, []);
@@ -539,6 +582,8 @@ export function ControlSystemProvider({ children }) {
updateKeyBinding,
resetKeyBindings,
registerInputState,
setManualDockAssistActive,
toggleManualDockAssist,
setSongNote,
sendSong,
startHorn,
@@ -565,6 +610,8 @@ export function ControlSystemProvider({ children }) {
updateKeyBinding,
resetKeyBindings,
registerInputState,
setManualDockAssistActive,
toggleManualDockAssist,
setSongNote,
sendSong,
startHorn,
+2 -8
View File
@@ -12,6 +12,8 @@ export const DRIVE_LIMITS = {
boostSpeed: 400,
};
export const MANUAL_DOCK_ASSIST_MAX_SPEED = 60;
export const COMMAND_DELAY_MS = 100;
export const SONG_NOTE_RANGE = [31, 127];
@@ -78,12 +80,4 @@ export const DEFAULT_MACROS = [
{ type: 'drive', speeds: { left: 0, right: 0 } },
],
},
{
id: 'seek-dock',
label: 'Dock',
description: 'Send the seek dock command.',
steps: [
{ type: 'oi', command: 'dock' }
],
},
];
+16
View File
@@ -43,6 +43,12 @@ function createMicState() {
};
}
function createManualDockAssistState() {
return {
active: false,
};
}
export const initialControlState = {
roverId: null,
mode: 'drive',
@@ -52,6 +58,7 @@ export const initialControlState = {
song: createSongState(),
horn: createHornState(),
mic: createMicState(),
manualDockAssist: createManualDockAssistState(),
lastControlIntentAt: 0,
macros: DEFAULT_MACROS,
keymap: DEFAULT_KEYMAP,
@@ -69,6 +76,7 @@ export function controlReducer(state, action) {
song: action.payload ? state.song : createSongState(),
horn: action.payload ? state.horn : createHornState(),
mic: action.payload ? state.mic : createMicState(),
manualDockAssist: action.payload ? state.manualDockAssist : createManualDockAssistState(),
lastControlIntentAt: action.payload ? state.lastControlIntentAt : 0,
};
case 'control/set-mode':
@@ -187,6 +195,14 @@ export function controlReducer(state, action) {
pttActive: Boolean(action.payload),
},
};
case 'control/set-manual-dock-assist':
return {
...state,
manualDockAssist: {
...(state.manualDockAssist || createManualDockAssistState()),
active: Boolean(action.payload),
},
};
default:
return state;
}
@@ -11,6 +11,7 @@ import {
} from './gamepadBindings.js';
import { subscribeGamepadHub } from './gamepadHub.js';
import { isTextEntryActive } from './inputFocusUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
const SOURCE = 'gamepad';
const ZERO_VECTOR = { x: 0, y: 0, boost: false };
@@ -58,6 +59,7 @@ export default function GamepadInputManager() {
registerInputState,
},
} = useControlSystem();
const dockAssist = useManualDockAssist();
const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace(
'gamepad',
GAMEPAD_SETTINGS_DEFAULTS,
@@ -243,6 +245,7 @@ export default function GamepadInputManager() {
}
if (outputs.buttons.driveMacro && handleButtonEdge('driveMacro', true)) {
dockAssist.exitAssist();
setMode('drive');
runMacro('drive-sequence');
} else if (!outputs.buttons.driveMacro) {
@@ -250,8 +253,7 @@ export default function GamepadInputManager() {
}
if (outputs.buttons.dockMacro && handleButtonEdge('dockMacro', true)) {
setMode('dock');
runMacro('seek-dock');
dockAssist.toggleAssist();
} else if (!outputs.buttons.dockMacro) {
handleButtonEdge('dockMacro', false);
}
@@ -292,6 +294,7 @@ export default function GamepadInputManager() {
setDriveVector,
setMode,
toggleNightVision,
dockAssist,
]);
return null;
@@ -9,6 +9,7 @@ import { isKeyboardCaptureLocked } from './keyboardCaptureLock.js';
import { isTextInputElement } from './inputFocusUtils.js';
import { useSettingsNamespace } from '../../settings/index.js';
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import {
SONG_DEFAULT_DURATION,
SONG_DEFAULT_NOTE,
@@ -137,6 +138,7 @@ export default function KeyboardInputManager() {
},
} = useControlSystem();
const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null);
const dockAssist = useManualDockAssist();
const { homeAssistantSetState } = useSessionActions();
const { focusChat, blurChat, isChatFocused } = useChat();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
@@ -372,11 +374,11 @@ export default function KeyboardInputManager() {
if (newlyPressed.length > 0) {
if (newlyPressed.some((token) => keymap.driveMacro?.has(token))) {
dockAssist.exitAssist();
setMode('drive');
runMacro('drive-sequence');
} else if (newlyPressed.some((token) => keymap.dockMacro?.has(token))) {
setMode('dock');
runMacro('seek-dock');
dockAssist.toggleAssist();
} else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) {
toggleNightVision();
} else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) {
@@ -448,6 +450,7 @@ export default function KeyboardInputManager() {
startHorn,
stopHorn,
triggerHomeAssistantCycle,
dockAssist,
]);
const latestResetAllRef = useRef(resetAll);
@@ -0,0 +1,67 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useControlSystem } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
export function useManualDockAssist(options = {}) {
const { manageLifecycle = false } = options;
const {
state: { roverId, manualDockAssist },
actions,
} = useControlSystem();
const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {};
const chargingLabel = sensors?.chargingState?.label || '';
const docked = Boolean(sensors?.chargingSources?.homeBase);
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
const active = Boolean(manualDockAssist?.active);
const wasDockedRef = useRef(false);
const enterAssist = useCallback(() => {
actions.setManualDockAssistActive(true);
}, [actions]);
const exitAssist = useCallback(() => {
actions.setManualDockAssistActive(false);
}, [actions]);
const toggleAssist = useCallback(() => {
actions.toggleManualDockAssist();
}, [actions]);
useEffect(() => {
if (!manageLifecycle) return;
const justDocked = docked && !wasDockedRef.current;
const justUndocked = !docked && wasDockedRef.current;
if (active && justDocked) {
actions.sendSong([{ note: 84, duration: 6 }], { slot: 1 });
} else if (active && justUndocked) {
actions.sendSong([{ note: 72, duration: 6 }], { slot: 1 });
}
wasDockedRef.current = docked;
}, [actions, active, docked, manageLifecycle]);
useEffect(() => {
if (!manageLifecycle || !active || !charging) return;
exitAssist();
}, [active, charging, exitAssist, manageLifecycle]);
const statusLabel = charging ? 'Docked and charging' : docked ? 'Docked' : 'Docking assist active';
const statusTone = charging ? 'good' : docked ? 'warn' : 'active';
const visible = active || docked;
return useMemo(
() => ({
active,
docked,
charging,
cameraLocked: active,
visible,
statusLabel,
statusTone,
enterAssist,
exitAssist,
toggleAssist,
}),
[active, charging, docked, enterAssist, exitAssist, statusLabel, statusTone, toggleAssist, visible],
);
}
+11 -10
View File
@@ -27,9 +27,10 @@ export const HELP_CONTENT = {
type: 'list',
title: 'Docking the rover',
items: [
'Line up the rover to the dock, about a foot away, then:',
{ segments: ['Press the "Dock and Charge" button onscreen, or press ', { action: 'dockMacro' }, ' on your keyboard to start docking.'] },
'Wait for the rover to confirm it is docked and charging before leaving it unattended.',
'If the rover shows "Docking in Progress", it is already auto-seeking the dock.',
'To dock manually, enter Docking Assist from the drive panel.',
{ segments: ['Press "Enter Docking Assist", or press ', { action: 'dockMacro' }, '.'] },
'In assist mode, camera tilts down and driving speed is limited for precise alignment.',
],
},
],
@@ -56,7 +57,7 @@ export const HELP_CONTENT = {
title: 'Rover modes & chat',
items: [
{ action: 'driveMacro', label: 'Drive macro' },
{ action: 'dockMacro', label: 'Dock macro' },
{ action: 'dockMacro', label: 'Docking assist toggle' },
{ action: 'chatFocus', label: 'Chat focus' },
],
},
@@ -133,9 +134,9 @@ export const HELP_CONTENT = {
type: 'list',
title: 'Docking the rover',
items: [
'Line up the rover to the dock, about a foot away, then:',
{ segments: ['Press the "Dock and Charge" button onscreen to start docking.'] },
'Wait for the rover to confirm it is docked and charging before leaving it unattended.',
'If you see "Docking in Progress", the rover is currently auto-seeking the dock.',
{ segments: ['For manual docking, press "Enter Docking Assist".'] },
'Assist mode tilts camera down and limits speed for precise alignment.',
],
}
],
@@ -163,9 +164,9 @@ export const HELP_CONTENT = {
type: 'list',
title: 'Docking the rover',
items: [
'Line up the rover to the dock, about a foot away, then:',
{ segments: ['Press the "Dock and Charge" or the "Dock" button onscreen to start docking.'] },
'Wait for the rover to confirm it is docked and charging before leaving it unattended.',
'If you see "Docking in Progress", the rover is currently auto-seeking the dock.',
{ segments: ['For manual docking, press "Enter Docking Assist" (or "Dock" button).'] },
'Assist mode tilts camera down and limits speed for precise alignment.',
],
}
],