refactorio

This commit is contained in:
legop3
2026-05-17 21:35:33 -04:00
parent 31f98a8e32
commit 27f31531ac
10 changed files with 234 additions and 196 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<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="Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-X92JgDHr.js"></script> <script type="module" crossorigin src="/assets/index-D-6ytlM1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CkqCBh_q.css"> <link rel="stylesheet" crossorigin href="/assets/index-CkqCBh_q.css">
</head> </head>
<body> <body>
+9 -22
View File
@@ -1,10 +1,11 @@
// Drive Dock Action // Drive Dock Action
// Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file. // Purpose: Defines the Drive Dock Action module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useRef, useState } from 'react'; import { useMemo, useState } from 'react';
import { useControlSystem } from '../../controls/index.js'; import { useControlSystem } from '../../controls/index.js';
import { useTelemetryFrame } from '../../context/TelemetryContext.jsx'; import { useTelemetryFrame } from '../../context/TelemetryContext.jsx';
import { formatKeyLabel } from '../../controls/keymapUtils.js'; import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
export function deriveDriveDockState(frame) { export function deriveDriveDockState(frame) {
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
@@ -103,17 +104,17 @@ export default function DriveDockAction({
}) { }) {
const isMobile = layout === 'mobile'; const isMobile = layout === 'mobile';
const { const {
state: { roverId, keymap, manualDockAssist }, state: { roverId, keymap },
actions, actions,
} = useControlSystem(); } = useControlSystem();
const dockAssist = useManualDockAssist();
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const state = driveDockState ?? deriveDriveDockState(frame); const state = driveDockState ?? deriveDriveDockState(frame);
const { driving, docked, charging, dockingInProgress } = state; const { driving, docked, charging, dockingInProgress } = state;
const [pending, setPending] = useState(null); const [pending, setPending] = useState(null);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const manualAssistActive = Boolean(manualDockAssist?.active); const manualAssistActive = Boolean(dockAssist.active);
const wasDockedRef = useRef(false);
const driveDisabled = !roverId || pending !== null; const driveDisabled = !roverId || pending !== null;
const dockDisabled = !roverId || pending !== null; const dockDisabled = !roverId || pending !== null;
@@ -139,25 +140,11 @@ export default function DriveDockAction({
const chargeValue = charging ? 'Charging' : docked ? 'Charging soon...' : '—'; const chargeValue = charging ? 'Charging' : docked ? 'Charging soon...' : '—';
const chargeTone = charging ? 'good' : docked ? 'warn' : 'bad'; const chargeTone = charging ? 'good' : docked ? 'warn' : 'bad';
useEffect(() => {
const justDocked = docked && !wasDockedRef.current;
if (manualAssistActive && justDocked) {
actions.sendSong([{ note: 84, duration: 6 }], { slot: 1 });
}
wasDockedRef.current = docked;
}, [actions, docked, manualAssistActive]);
useEffect(() => {
if (!manualAssistActive) return;
if (!charging) return;
actions.setManualDockAssistActive(false);
}, [actions, charging, manualAssistActive]);
const handleReturnToDrive = async () => { const handleReturnToDrive = async () => {
if (!roverId || pending) return; if (!roverId || pending) return;
setPending('drive'); setPending('drive');
try { try {
actions.setManualDockAssistActive(false); dockAssist.exitAssist();
actions.setMode('drive'); actions.setMode('drive');
await actions.runMacro('drive-sequence'); await actions.runMacro('drive-sequence');
} catch (err) { } catch (err) {
@@ -173,7 +160,7 @@ export default function DriveDockAction({
setShowModal(false); setShowModal(false);
setPending('drive'); setPending('drive');
try { try {
actions.setManualDockAssistActive(false); dockAssist.exitAssist();
actions.setMode('drive'); actions.setMode('drive');
await actions.runMacro('drive-sequence'); await actions.runMacro('drive-sequence');
} catch (err) { } catch (err) {
@@ -187,7 +174,7 @@ export default function DriveDockAction({
if (!roverId || pending) return; if (!roverId || pending) return;
setPending('dock'); setPending('dock');
try { try {
actions.setManualDockAssistActive(true); dockAssist.enterAssist();
} catch (err) { } catch (err) {
alert(err.message); alert(err.message);
} finally { } finally {
@@ -200,7 +187,7 @@ export default function DriveDockAction({
const handleOpenDock = () => { const handleOpenDock = () => {
if (dockDisabled) return; if (dockDisabled) return;
if (manualAssistActive) { if (manualAssistActive) {
actions.setManualDockAssistActive(false); dockAssist.exitAssist();
return; return;
} }
setShowModal(true); setShowModal(true);
@@ -1,31 +1,15 @@
import React from 'react'; import React from 'react';
import { useControlSystem } from '../../../controls/index.js'; import { useManualDockAssist } from '../../../features/manualDockAssist/useManualDockAssist.js';
import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useTelemetryFrame } from '../../../context/TelemetryContext.jsx';
function ManualDockAssistOverlay({ mobileHud = false }) { function ManualDockAssistOverlay({ mobileHud = false }) {
const { const { visible, statusLabel, statusTone } = useManualDockAssist({ manageLifecycle: true });
state: { manualDockAssist }, if (!visible) return null;
} = useControlSystem(); const toneClass =
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null); statusTone === 'good'
const frame = useTelemetryFrame(roverId); ? 'border-emerald-200/70 bg-emerald-900/85 text-emerald-50'
const sensors = frame?.sensors || {}; : statusTone === 'warn'
const chargingLabel = sensors?.chargingState?.label || ''; ? 'border-amber-200/70 bg-amber-900/85 text-amber-50'
const docked = Boolean(sensors?.chargingSources?.homeBase); : 'border-indigo-200/70 bg-indigo-900/85 text-indigo-50';
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
const active = Boolean(manualDockAssist?.active);
if (!active && !docked) return null;
const status = charging
? 'Docked and charging'
: docked
? 'Docked'
: 'Docking assist active';
const toneClass = charging
? 'border-emerald-200/70 bg-emerald-900/85 text-emerald-50'
: docked
? 'border-amber-200/70 bg-amber-900/85 text-amber-50'
: 'border-indigo-200/70 bg-indigo-900/85 text-indigo-50';
return ( return (
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">
@@ -34,7 +18,7 @@ function ManualDockAssistOverlay({ mobileHud = false }) {
mobileHud ? 'text-[0.65rem]' : 'text-xs' mobileHud ? 'text-[0.65rem]' : 'text-xs'
}`} }`}
> >
{status} {statusLabel}
</div> </div>
</div> </div>
); );
@@ -18,6 +18,7 @@ import {
AUX_ALL_FORWARD, AUX_ALL_FORWARD,
AUX_ALL_BACKWARD, AUX_ALL_BACKWARD,
} from './constants.js'; } from './constants.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
function MobileJoystickPanel({ layout }) { function MobileJoystickPanel({ layout }) {
const { const {
@@ -95,10 +96,11 @@ function MobileJoystickPanel({ layout }) {
function MobileActionsColumnContent({ layout }) { function MobileActionsColumnContent({ layout }) {
const { const {
state: { roverId, camera, horn, manualDockAssist }, state: { roverId, camera, horn },
pipeline, pipeline,
actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn }, actions: { setServoAngle, setNightVision, setAuxMotors, startHorn, stopHorn },
} = useControlSystem(); } = useControlSystem();
const dockAssist = useManualDockAssist();
const disabled = !roverId; const disabled = !roverId;
const activeRef = useRef(null); const activeRef = useRef(null);
const cameraConfig = camera?.config; const cameraConfig = camera?.config;
@@ -115,7 +117,7 @@ function MobileActionsColumnContent({ layout }) {
: typeof cameraConfig?.homeAngle === 'number' : typeof cameraConfig?.homeAngle === 'number'
? cameraConfig.homeAngle ? cameraConfig.homeAngle
: (cameraMin + cameraMax) / 2; : (cameraMin + cameraMax) / 2;
const cameraDisabled = Boolean(disabled || manualDockAssist?.active); const cameraDisabled = Boolean(disabled || dockAssist.cameraLocked);
const handleNightVisionToggle = useCallback( const handleNightVisionToggle = useCallback(
(nextOn) => { (nextOn) => {
+4 -2
View File
@@ -24,6 +24,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx'; import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import { useState } from 'react'; import { useState } from 'react';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
function TopDownMapPanel() { function TopDownMapPanel() {
const { const {
@@ -43,10 +44,11 @@ function TopDownMapPanel() {
function DriveDockPanel() { function DriveDockPanel() {
const { const {
state: { roverId, keymap, camera, horn, manualDockAssist }, state: { roverId, keymap, camera, horn },
pipeline, pipeline,
actions: { setServoAngle, setNightVision, startHorn, stopHorn }, actions: { setServoAngle, setNightVision, startHorn, stopHorn },
} = useControlSystem(); } = useControlSystem();
const dockAssist = useManualDockAssist();
const driveDockState = useDriveDockState(roverId); const driveDockState = useDriveDockState(roverId);
const hideInlineControls = driveDockState.docked && !driveDockState.driving; const hideInlineControls = driveDockState.docked && !driveDockState.driving;
@@ -68,7 +70,7 @@ function DriveDockPanel() {
const hornLabel = formatKeyLabel(keymap?.hornHonk?.[0]); const hornLabel = formatKeyLabel(keymap?.hornHonk?.[0]);
const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]); const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]);
const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]); const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]);
const cameraDisabled = Boolean(!roverId || manualDockAssist?.active); const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
return ( return (
<section className="panel-section grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] gap-0.5"> <section className="panel-section grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto] gap-0.5">
@@ -11,6 +11,7 @@ import {
} from './gamepadBindings.js'; } from './gamepadBindings.js';
import { subscribeGamepadHub } from './gamepadHub.js'; import { subscribeGamepadHub } from './gamepadHub.js';
import { isTextEntryActive } from './inputFocusUtils.js'; import { isTextEntryActive } from './inputFocusUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
const SOURCE = 'gamepad'; const SOURCE = 'gamepad';
const ZERO_VECTOR = { x: 0, y: 0, boost: false }; const ZERO_VECTOR = { x: 0, y: 0, boost: false };
@@ -50,8 +51,6 @@ export default function GamepadInputManager() {
state, state,
actions: { actions: {
setMode, setMode,
setManualDockAssistActive,
toggleManualDockAssist,
setDriveVector, setDriveVector,
setAuxMotors, setAuxMotors,
setServoAngle, setServoAngle,
@@ -60,6 +59,7 @@ export default function GamepadInputManager() {
registerInputState, registerInputState,
}, },
} = useControlSystem(); } = useControlSystem();
const dockAssist = useManualDockAssist();
const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace( const { value: gamepadSettings, save: saveGamepadSettings } = useSettingsNamespace(
'gamepad', 'gamepad',
GAMEPAD_SETTINGS_DEFAULTS, GAMEPAD_SETTINGS_DEFAULTS,
@@ -245,7 +245,7 @@ export default function GamepadInputManager() {
} }
if (outputs.buttons.driveMacro && handleButtonEdge('driveMacro', true)) { if (outputs.buttons.driveMacro && handleButtonEdge('driveMacro', true)) {
setManualDockAssistActive(false); dockAssist.exitAssist();
setMode('drive'); setMode('drive');
runMacro('drive-sequence'); runMacro('drive-sequence');
} else if (!outputs.buttons.driveMacro) { } else if (!outputs.buttons.driveMacro) {
@@ -253,7 +253,7 @@ export default function GamepadInputManager() {
} }
if (outputs.buttons.dockMacro && handleButtonEdge('dockMacro', true)) { if (outputs.buttons.dockMacro && handleButtonEdge('dockMacro', true)) {
toggleManualDockAssist(); dockAssist.toggleAssist();
} else if (!outputs.buttons.dockMacro) { } else if (!outputs.buttons.dockMacro) {
handleButtonEdge('dockMacro', false); handleButtonEdge('dockMacro', false);
} }
@@ -290,12 +290,11 @@ export default function GamepadInputManager() {
handleCameraAxis, handleCameraAxis,
registerInputState, registerInputState,
runMacro, runMacro,
setManualDockAssistActive,
setAuxMotors, setAuxMotors,
setDriveVector, setDriveVector,
setMode, setMode,
toggleManualDockAssist,
toggleNightVision, toggleNightVision,
dockAssist,
]); ]);
return null; return null;
@@ -9,6 +9,7 @@ import { isKeyboardCaptureLocked } from './keyboardCaptureLock.js';
import { isTextInputElement } from './inputFocusUtils.js'; import { isTextInputElement } from './inputFocusUtils.js';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js'; import { INPUT_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { import {
SONG_DEFAULT_DURATION, SONG_DEFAULT_DURATION,
SONG_DEFAULT_NOTE, SONG_DEFAULT_NOTE,
@@ -122,8 +123,6 @@ export default function KeyboardInputManager() {
state, state,
actions: { actions: {
setMode, setMode,
setManualDockAssistActive,
toggleManualDockAssist,
setDriveVector, setDriveVector,
setAuxMotors, setAuxMotors,
nudgeServo, nudgeServo,
@@ -139,6 +138,7 @@ export default function KeyboardInputManager() {
}, },
} = useControlSystem(); } = useControlSystem();
const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null); const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null);
const dockAssist = useManualDockAssist();
const { homeAssistantSetState } = useSessionActions(); const { homeAssistantSetState } = useSessionActions();
const { focusChat, blurChat, isChatFocused } = useChat(); const { focusChat, blurChat, isChatFocused } = useChat();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS); const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
@@ -374,11 +374,11 @@ export default function KeyboardInputManager() {
if (newlyPressed.length > 0) { if (newlyPressed.length > 0) {
if (newlyPressed.some((token) => keymap.driveMacro?.has(token))) { if (newlyPressed.some((token) => keymap.driveMacro?.has(token))) {
setManualDockAssistActive(false); dockAssist.exitAssist();
setMode('drive'); setMode('drive');
runMacro('drive-sequence'); runMacro('drive-sequence');
} else if (newlyPressed.some((token) => keymap.dockMacro?.has(token))) { } else if (newlyPressed.some((token) => keymap.dockMacro?.has(token))) {
toggleManualDockAssist(); dockAssist.toggleAssist();
} else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) { } else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) {
toggleNightVision(); toggleNightVision();
} else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) { } else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) {
@@ -443,15 +443,14 @@ export default function KeyboardInputManager() {
keymap.micPtt, keymap.micPtt,
resetAll, resetAll,
runMacro, runMacro,
setManualDockAssistActive,
setMicPttActive, setMicPttActive,
setMode, setMode,
stopAllMotion, stopAllMotion,
stopSongLoop, stopSongLoop,
startHorn, startHorn,
stopHorn, stopHorn,
toggleManualDockAssist,
triggerHomeAssistantCycle, triggerHomeAssistantCycle,
dockAssist,
]); ]);
const latestResetAllRef = useRef(resetAll); const latestResetAllRef = useRef(resetAll);
@@ -0,0 +1,65 @@
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;
if (active && justDocked) {
actions.sendSong([{ note: 84, 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],
);
}