mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 18:58:49 -05:00
basically moving docking flows to the server instead of being in the web UI
This commit is contained in:
@@ -11,10 +11,10 @@ export function deriveDriveDockStateFromTelemetry(dockTelemetry) {
|
||||
const oiNormalized = oiLabel.toLowerCase();
|
||||
const chargingLabel = dockTelemetry?.chargingStateLabel || '';
|
||||
const docked = Boolean(dockTelemetry?.homeBase);
|
||||
const charging = docked && chargingLabel.toLowerCase() !== 'not charging' && chargingLabel !== '';
|
||||
const driving = oiNormalized === 'full';
|
||||
const charging = dockTelemetry?.dockingPhase === 'charging';
|
||||
const driving = oiNormalized === 'full' && !dockTelemetry?.movementLocked;
|
||||
const dockedNotCharging = docked && !charging;
|
||||
const dockingInProgress = !docked && !charging && oiNormalized === 'passive';
|
||||
const dockingInProgress = !docked && !charging && oiNormalized === 'passive' && dockTelemetry?.dockingPhase !== 'undocking';
|
||||
return { driving, docked, charging, dockedNotCharging, dockingInProgress, oiLabel, chargingLabel };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Drive Dock Action
|
||||
// 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.
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import '../MobileControls/mobileControls.css';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useTelemetrySelector } from '../../context/TelemetryContext.jsx';
|
||||
import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetryViews.js';
|
||||
import ControlHint from '../ControlHint/index.jsx';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import { deriveDriveDockStateFromTelemetry } from './driveDockState.js';
|
||||
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
|
||||
|
||||
@@ -91,14 +90,13 @@ export default function DriveDockAction({
|
||||
const isMobile = layout === 'mobile';
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const actions = useControlActions();
|
||||
const dockAssist = useManualDockAssist();
|
||||
const dockTelemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
|
||||
const state = driveDockState ?? deriveDriveDockStateFromTelemetry(dockTelemetry);
|
||||
const { driving, docked, charging, dockingInProgress } = state;
|
||||
const [pending, setPending] = useState(null);
|
||||
const pending = dockTelemetry.dockingPhase === 'undocking' ? 'drive' : null;
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const manualAssistActive = Boolean(dockAssist.active);
|
||||
const manualAssistActive = Boolean(dockTelemetry.cameraLocked);
|
||||
|
||||
const driveDisabled = !roverId || pending !== null;
|
||||
const dockDisabled = !roverId || pending !== null;
|
||||
@@ -124,64 +122,37 @@ export default function DriveDockAction({
|
||||
const chargeValue = charging ? 'Charging' : docked ? 'Charging soon...' : '—';
|
||||
const chargeTone = charging ? 'good' : docked ? 'warn' : 'bad';
|
||||
|
||||
const handleReturnToDrive = async () => {
|
||||
if (!roverId || pending) return;
|
||||
if (isMobile) triggerTouchHaptic('button');
|
||||
setPending('drive');
|
||||
try {
|
||||
dockAssist.exitAssist();
|
||||
actions.setMode('drive');
|
||||
await actions.runMacro('drive-sequence');
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setPending(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartDrive = async () => {
|
||||
const handleStartDrive = () => {
|
||||
if (!roverId || pending) return;
|
||||
if (isMobile) triggerTouchHaptic('button');
|
||||
setConfirmOpen(false);
|
||||
setShowModal(false);
|
||||
setPending('drive');
|
||||
try {
|
||||
dockAssist.exitAssist();
|
||||
actions.setMode('drive');
|
||||
await actions.runMacro('drive-sequence');
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setPending(null);
|
||||
}
|
||||
actions.undock();
|
||||
};
|
||||
|
||||
const handleConfirmDock = async () => {
|
||||
const handleConfirmDock = () => {
|
||||
if (!roverId || pending) return;
|
||||
if (isMobile) triggerTouchHaptic('button');
|
||||
setPending('dock');
|
||||
try {
|
||||
dockAssist.enterAssist();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setPending(null);
|
||||
setConfirmOpen(false);
|
||||
setShowModal(false);
|
||||
}
|
||||
actions.sendCommand('enterDocking');
|
||||
setConfirmOpen(false);
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
const handleOpenDock = () => {
|
||||
if (dockDisabled) return;
|
||||
if (isMobile) triggerTouchHaptic('button');
|
||||
if (manualAssistActive) {
|
||||
dockAssist.exitAssist();
|
||||
actions.sendCommand('cancelDocking');
|
||||
return;
|
||||
}
|
||||
setShowModal(true);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dockTelemetry.dockingError) alert(dockTelemetry.dockingError);
|
||||
}, [dockTelemetry.dockingError]);
|
||||
|
||||
// The drive/dock card appears inside the mobile controls and can be held or
|
||||
// tapped repeatedly; attach mobile touch suppression directly to the card so
|
||||
// Safari does not select labels or open callouts during those interactions.
|
||||
@@ -254,7 +225,7 @@ export default function DriveDockAction({
|
||||
<button
|
||||
type="button"
|
||||
disabled={driveDisabled}
|
||||
onClick={handleReturnToDrive}
|
||||
onClick={handleStartDrive}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
className={`${baseCardClasses} ${filledHeight} ${compactHeight} ${ctaText} ${layoutClass} ${ctaSize} ${amberCta}`}
|
||||
>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function DriverVideo({ layoutFormat = 'desktop' }) {
|
||||
mobileHud={mobileHud}
|
||||
labelScale={1}
|
||||
/>
|
||||
<ManualDockAssistOverlay mobileHud={mobileHud} />
|
||||
<ManualDockAssistOverlay roverId={roverId} mobileHud={mobileHud} />
|
||||
<HudChatInput compact={mobileHud} />
|
||||
<OvercurrentOverlay compact={mobileHud} />
|
||||
<LowBatteryOverlay compact={mobileHud} />
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ACTIONS = [
|
||||
{ id: 'allAux', label: 'All cleaning motors', kind: 'button', section: 'Aux buttons' },
|
||||
{ id: 'mainReverse', label: 'Main reverse toggle', kind: 'button', section: 'Brush toggles' },
|
||||
{ id: 'sideReverse', label: 'Side reverse toggle', kind: 'button', section: 'Brush toggles' },
|
||||
{ id: 'driveMacro', label: 'Drive / undock sequence', kind: 'button', section: 'Mode controls' },
|
||||
{ id: 'driveMacro', label: 'Drive / undock', kind: 'button', section: 'Mode controls' },
|
||||
{ id: 'dockMacro', label: 'Manual docking assist', kind: 'button', section: 'Mode controls' },
|
||||
{ id: 'headlightToggle', label: 'Headlight toggle', kind: 'button', section: 'Camera' },
|
||||
{ id: 'laserToggle', label: 'Laser toggle', kind: 'button', section: 'Camera' },
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { dockTelemetryEqual, selectDockTelemetry } from '../../../context/telemetryViews.js';
|
||||
import { useTelemetrySelector } from '../../../context/TelemetryContext.jsx';
|
||||
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 });
|
||||
function ManualDockAssistOverlay({ roverId, mobileHud = false }) {
|
||||
const telemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
|
||||
const active = telemetry.cameraLocked;
|
||||
const charging = telemetry.dockingPhase === 'charging';
|
||||
const visible = active || telemetry.homeBase;
|
||||
const statusLabel = charging ? 'Docked and charging' : telemetry.homeBase ? 'Docked' : 'Docking assist active';
|
||||
const statusTone = charging ? 'good' : telemetry.homeBase ? 'warn' : 'active';
|
||||
const [popupMessage, setPopupMessage] = useState('');
|
||||
const timerRef = useRef(null);
|
||||
const prevActiveRef = useRef(active);
|
||||
const [previousActive, setPreviousActive] = useState(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]);
|
||||
// These messages are presentation only; telemetry owns the operation itself.
|
||||
if (previousActive !== active) {
|
||||
setPreviousActive(active);
|
||||
if (active) setPopupMessage('Dock assist mode enabled');
|
||||
else if (charging) setPopupMessage('Docking successful! Thank you!');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!popupMessage) return undefined;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
|
||||
import { selectDockTelemetry, dockTelemetryEqual } from '../../../../context/telemetryViews.js';
|
||||
// Bottom-right Corner Pod
|
||||
// Purpose: Provides a compact circular camera-tilt control using the existing servo command path.
|
||||
import { useCallback } from 'react';
|
||||
@@ -31,7 +33,7 @@ export default function BottomRightPod({ roverId }) {
|
||||
const layout = useLayout();
|
||||
const [open, setOpen] = usePodVisibility('camera', true);
|
||||
const camera = useControlSelector((control) => control.state.camera);
|
||||
const dockAssistActive = useControlSelector((control) => Boolean(control.state.manualDockAssist?.active));
|
||||
const { cameraLocked } = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
|
||||
const { setServoAngle } = useControlActions();
|
||||
const canControl = useCanControlRover(roverId);
|
||||
const config = camera?.config;
|
||||
@@ -47,7 +49,7 @@ export default function BottomRightPod({ roverId }) {
|
||||
const cameraPodOpen = showCameraControls && enabled && open;
|
||||
|
||||
const updateFromPointer = useCallback((event) => {
|
||||
if (!canControl || !enabled || dockAssistActive || !(max > min)) return;
|
||||
if (!canControl || !enabled || cameraLocked || !(max > min)) return;
|
||||
const bounds = event.currentTarget.getBoundingClientRect();
|
||||
const x = ((event.clientX - bounds.left) / bounds.width) * 144;
|
||||
const y = ((event.clientY - bounds.top) / bounds.height) * 144;
|
||||
@@ -64,7 +66,7 @@ export default function BottomRightPod({ roverId }) {
|
||||
}
|
||||
const nextFraction = arcDegrees / ARC_SWEEP_DEGREES;
|
||||
setServoAngle(min + nextFraction * (max - min));
|
||||
}, [canControl, dockAssistActive, enabled, max, min, setServoAngle]);
|
||||
}, [canControl, cameraLocked, enabled, max, min, setServoAngle]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useControlActions } from '../../../../controls/index.js';
|
||||
import ControlHint from '../../../ControlHint/index.jsx';
|
||||
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
|
||||
import { dockTelemetryEqual, selectDockTelemetry } from '../../../../context/telemetryViews.js';
|
||||
import { useManualDockAssist } from '../../../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import { useSettingsNamespace } from '../../../../settings/index.js';
|
||||
import useCanControlRover from '../../../../hooks/useCanControlRover.js';
|
||||
import { useLayout } from '../../../../layouts/LayoutContext.jsx';
|
||||
@@ -253,9 +252,6 @@ export default function DockingHud({ roverId }) {
|
||||
const layout = useLayout();
|
||||
const actions = useControlActions();
|
||||
const dockTelemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
|
||||
// This replaces ManualDockAssistOverlay as the current HUD's one lifecycle owner. It preserves the
|
||||
// success sounds, camera positioning, speed cap, and automatic exit after charging begins.
|
||||
const dockAssist = useManualDockAssist({ manageLifecycle: true });
|
||||
const canControl = useCanControlRover(roverId);
|
||||
const batteryState = useSessionSelector((state) => {
|
||||
const rover = (state.session?.roster || []).find((entry) => String(entry.id) === String(roverId));
|
||||
@@ -268,15 +264,14 @@ export default function DockingHud({ roverId }) {
|
||||
const [dockExpansionOpen, setDockExpansionOpen] = usePodVisibility('dockAssist', true);
|
||||
// The action name is presentation state as well as busy state. Keeping the
|
||||
// reason prevents a passive, already-undocked rover from ever saying "Undocking".
|
||||
const [pendingAction, setPendingAction] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [showUndockTransition, setShowUndockTransition] = useState(false);
|
||||
|
||||
const docked = Boolean(dockTelemetry?.homeBase);
|
||||
const oiMode = String(dockTelemetry?.oiModeLabel || '').toLowerCase();
|
||||
// The established UI contract treats exactly passive + undocked as the Roomba's
|
||||
// autonomous docking attempt. Unknown telemetry must not fabricate that state.
|
||||
const autoDocking = !docked && !dockAssist.active && oiMode === 'passive';
|
||||
const autoDocking = !docked && !dockTelemetry.cameraLocked && oiMode === 'passive';
|
||||
const pendingAction = (dockTelemetry.dockingPhase === 'undocking') ? (docked ? 'undocking' : 'resuming') : null;
|
||||
const pending = pendingAction !== null;
|
||||
/* Mobile already presents its own touch-oriented driving controls. The docked
|
||||
action therefore keeps its plain-language instruction without advertising a
|
||||
@@ -287,55 +282,22 @@ export default function DockingHud({ roverId }) {
|
||||
// The camera arc is the shared circular-pod reference size. Keep the dock expansion flush
|
||||
// against the battery shell after enlarging that gauge to the same 8.5-rem footprint.
|
||||
const cornerOffsetClass = batteryPodOpen ? 'right-34' : 'right-0';
|
||||
const previousDockedRef = useRef(docked);
|
||||
const [previousDocked, setPreviousDocked] = useState(docked);
|
||||
const finishUndockTransition = useCallback(() => setShowUndockTransition(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
const wasDocked = previousDockedRef.current;
|
||||
// Only a real live transition plays the cue. An already-undocked rover must not animate
|
||||
// merely because the user opened or refreshed the page.
|
||||
if (wasDocked && !docked) {
|
||||
setShowUndockTransition(true);
|
||||
} else if (docked) {
|
||||
setShowUndockTransition(false);
|
||||
}
|
||||
previousDockedRef.current = docked;
|
||||
}, [docked]);
|
||||
// Presentation-only transition; mounting with an undocked rover does not animate.
|
||||
if (previousDocked !== docked) {
|
||||
setPreviousDocked(docked);
|
||||
setShowUndockTransition(!docked);
|
||||
}
|
||||
|
||||
const startDriving = async (action) => {
|
||||
const startDriving = () => {
|
||||
if (!roverId || pending || !canControl) return;
|
||||
setPendingAction(action);
|
||||
setError('');
|
||||
try {
|
||||
// The established drive sequence is the canonical undock path: it restores the camera,
|
||||
// enters full Open Interface mode, and performs the short physical back-away from the dock.
|
||||
dockAssist.exitAssist();
|
||||
actions.setMode('drive');
|
||||
await actions.runMacro('drive-sequence');
|
||||
} catch (caughtError) {
|
||||
setError(caughtError?.message || 'Unable to start driving. Please try again.');
|
||||
} finally {
|
||||
setPendingAction(null);
|
||||
}
|
||||
actions.undock();
|
||||
};
|
||||
|
||||
const startUndocking = () => {
|
||||
startDriving('undocking');
|
||||
};
|
||||
const resumeDriving = () => {
|
||||
startDriving('resuming');
|
||||
};
|
||||
|
||||
const startDocking = () => {
|
||||
if (!roverId || pending || !canControl) return;
|
||||
setError('');
|
||||
try {
|
||||
// Enter on the first click. The explanation appears as the resulting active state instead
|
||||
// of forcing the user through a modal and a second confirmation action.
|
||||
dockAssist.enterAssist();
|
||||
} catch (caughtError) {
|
||||
setError(caughtError?.message || 'Unable to start dock assist. Please try again.');
|
||||
}
|
||||
actions.sendCommand('enterDocking');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -358,20 +320,20 @@ export default function DockingHud({ roverId }) {
|
||||
driveKeyLabel={driveKeyLabel}
|
||||
pending={pendingAction === 'undocking'}
|
||||
controlsDisabled={!canControl}
|
||||
error={error}
|
||||
onUndock={startUndocking}
|
||||
error={dockTelemetry.dockingError}
|
||||
onUndock={startDriving}
|
||||
/>
|
||||
) : autoDocking || pendingAction === 'resuming' ? (
|
||||
<AutoDockingAction
|
||||
driveKeyLabel={driveKeyLabel}
|
||||
pending={pendingAction === 'resuming'}
|
||||
controlsDisabled={!canControl}
|
||||
error={error}
|
||||
onResumeDriving={resumeDriving}
|
||||
error={dockTelemetry.dockingError}
|
||||
onResumeDriving={startDriving}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{dockAssist.active ? (
|
||||
{dockTelemetry.cameraLocked ? (
|
||||
/* Dock assist needs the unobscured camera image. A thin cyan frame communicates the
|
||||
temporary mode without adding another instruction surface over the video. */
|
||||
<div className="pointer-events-none absolute inset-0 z-50 ring-8 ring-inset ring-cyan-200/90" aria-hidden="true" />
|
||||
@@ -379,15 +341,15 @@ export default function DockingHud({ roverId }) {
|
||||
{/* Desktop keeps the compact corner entry point. Mobile starts assist from
|
||||
its dedicated control column, but once active it still mounts this component
|
||||
so the centered camera instruction and cancel action remain available. */}
|
||||
{layout === 'desktop' || dockAssist.active ? (
|
||||
{layout === 'desktop' || dockTelemetry.cameraLocked || dockTelemetry.dockingError ? (
|
||||
<DockAssistAction
|
||||
active={dockAssist.active}
|
||||
active={dockTelemetry.cameraLocked}
|
||||
pending={pending}
|
||||
controlsDisabled={!canControl}
|
||||
error={error}
|
||||
error={dockTelemetry.dockingError}
|
||||
dockKeyLabel={dockKeyLabel}
|
||||
onDock={startDocking}
|
||||
onCancel={dockAssist.exitAssist}
|
||||
onCancel={() => actions.sendCommand('cancelDocking')}
|
||||
cornerOffsetClass={cornerOffsetClass}
|
||||
open={dockExpansionOpen}
|
||||
onOpenChange={setDockExpansionOpen}
|
||||
|
||||
@@ -7,7 +7,6 @@ import './mobileControls.css';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useTelemetrySelector } from '../../context/TelemetryContext.jsx';
|
||||
import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetryViews.js';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import useCanControlRover from '../../hooks/useCanControlRover.js';
|
||||
import HornControl from '../HornControl/index.jsx';
|
||||
import GPIOToggleControl from '../GPIOToggleControl/index.jsx';
|
||||
@@ -33,7 +32,6 @@ function AuxColumnContent({ accessoriesAvailable, onShowAccessories }) {
|
||||
const laserState = useControlSelector((control) => control.pipeline?.laserState);
|
||||
const pipelineHorn = useControlSelector((control) => control.pipeline?.horn);
|
||||
const { setServoAngle, setHeadlight, setLaser, setAuxMotors, startHorn, stopHorn } = useControlActions();
|
||||
const dockAssist = useManualDockAssist();
|
||||
const dockTelemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
|
||||
const canControl = useCanControlRover(roverId);
|
||||
// Turn ownership is the common mutation boundary for every control in this
|
||||
@@ -44,7 +42,8 @@ function AuxColumnContent({ accessoriesAvailable, onShowAccessories }) {
|
||||
const drivingMode = String(dockTelemetry?.oiModeLabel || '').toLowerCase() === 'full';
|
||||
const vacuumDisabled = controlsDisabled
|
||||
|| docked
|
||||
|| (!drivingMode && !dockAssist.active);
|
||||
|| dockTelemetry.movementLocked
|
||||
|| (!drivingMode && !dockTelemetry.cameraLocked);
|
||||
const activeAuxButtonRef = useRef(null);
|
||||
const cameraConfig = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||
@@ -60,7 +59,7 @@ function AuxColumnContent({ accessoriesAvailable, onShowAccessories }) {
|
||||
: typeof cameraConfig?.homeAngle === 'number'
|
||||
? cameraConfig.homeAngle
|
||||
: (cameraMin + cameraMax) / 2;
|
||||
const cameraDisabled = Boolean(controlsDisabled || dockAssist.cameraLocked);
|
||||
const cameraDisabled = Boolean(controlsDisabled || dockTelemetry.cameraLocked);
|
||||
/*
|
||||
The mobile tilt track shares the same precision flag as desktop tilt. This
|
||||
keeps the servo fine-step behavior tied to the selected movement mode rather
|
||||
|
||||
@@ -2,19 +2,18 @@
|
||||
// Purpose: Assembles the mobile movement column, which is the right column by default.
|
||||
// Scope: Owns movement availability while the in-video DockingHud owns every dock/undock action.
|
||||
import { FaChargingStation } from 'react-icons/fa';
|
||||
import { useControlSelector } from '../../controls/index.js';
|
||||
import { useControlActions, useControlSelector } from '../../controls/index.js';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useTelemetrySelector } from '../../context/TelemetryContext.jsx';
|
||||
import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetryViews.js';
|
||||
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
|
||||
import useCanControlRover from '../../hooks/useCanControlRover.js';
|
||||
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
|
||||
import ControlPadPanel from './ControlPadPanel.jsx';
|
||||
|
||||
function MovementColumnContent({ layout }) {
|
||||
const roverId = useControlSelector((control) => control.state.roverId);
|
||||
const actions = useControlActions();
|
||||
const dockTelemetry = useTelemetrySelector(roverId, selectDockTelemetry, dockTelemetryEqual);
|
||||
const dockAssist = useManualDockAssist();
|
||||
const canControl = useCanControlRover(roverId);
|
||||
const batteryState = useSessionSelector((state) => {
|
||||
const rover = (state.session?.roster || []).find((entry) => String(entry.id) === String(roverId));
|
||||
@@ -25,20 +24,11 @@ function MovementColumnContent({ layout }) {
|
||||
const docked = Boolean(dockTelemetry?.homeBase);
|
||||
const drivingMode = String(dockTelemetry?.oiModeLabel || '').toLowerCase() === 'full';
|
||||
|
||||
/*
|
||||
DockingHud is now the only place where a user starts driving or enters docking
|
||||
assist. This column only decides whether touch movement is safe. Requiring both
|
||||
an undocked rover and full OI mode keeps the pad inert throughout the undock
|
||||
macro, including the interval where dock contact and OI mode change separately.
|
||||
|
||||
Manual assist is the deliberate exception to the normal OI-mode requirement:
|
||||
its entire purpose is to let the user drive onto the dock under the assist speed
|
||||
cap. Actual dock contact still disables movement immediately.
|
||||
*/
|
||||
const movementDisabled = !roverId
|
||||
|| !canControl
|
||||
|| docked
|
||||
|| (!drivingMode && !dockAssist.active);
|
||||
|| dockTelemetry.movementLocked
|
||||
|| (!drivingMode && !dockTelemetry.cameraLocked);
|
||||
// Both controls share the same hardware-availability boundary. Dock assist
|
||||
// changes their actions, not who is allowed to command the rover.
|
||||
const dockActionDisabled = movementDisabled;
|
||||
@@ -49,10 +39,10 @@ function MovementColumnContent({ layout }) {
|
||||
// Mobile deliberately enters assist on the first tap. The centered video HUD
|
||||
// provides the resulting instruction, so a confirmation modal would only add
|
||||
// friction between intent and the camera-guided docking task.
|
||||
if (dockAssist.active) {
|
||||
dockAssist.exitAssist();
|
||||
if (dockTelemetry.cameraLocked) {
|
||||
actions.sendCommand('cancelDocking');
|
||||
} else {
|
||||
dockAssist.enterAssist();
|
||||
actions.sendCommand('enterDocking');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +53,7 @@ function MovementColumnContent({ layout }) {
|
||||
disabled={dockActionDisabled}
|
||||
onClick={handleDockAction}
|
||||
className={`mobile-touch-control flex min-h-18 shrink-0 items-center justify-center gap-1.5 rounded-xl border-2 px-2 text-base font-semibold shadow-md transition disabled:cursor-not-allowed disabled:opacity-50 ${
|
||||
dockAssist.active
|
||||
dockTelemetry.cameraLocked
|
||||
? 'border-cyan-300/70 bg-cyan-900 text-cyan-50'
|
||||
: batteryUrgent
|
||||
? 'border-red-300/80 bg-red-950 text-red-50'
|
||||
@@ -75,7 +65,7 @@ function MovementColumnContent({ layout }) {
|
||||
<FaChargingStation className="text-lg" aria-hidden="true" />
|
||||
{/* The mobile column remains the only mobile entry point into dock assist. Battery
|
||||
severity makes that existing action more obvious without adding another control. */}
|
||||
<span>{dockAssist.active ? 'Exit dock assist' : batteryUrgent ? 'Dock now' : batteryLow ? 'Dock and charge soon' : 'Dock and charge'}</span>
|
||||
<span>{dockTelemetry.cameraLocked ? 'Exit dock assist' : batteryUrgent ? 'Dock now' : batteryLow ? 'Dock and charge soon' : 'Dock and charge'}</span>
|
||||
</button>
|
||||
{/* Keeping the pad mounted prevents the mobile columns from changing size while
|
||||
the centered video HUD explains and performs dock-related transitions. */}
|
||||
|
||||
@@ -411,7 +411,7 @@ export default function SettingsPanel() {
|
||||
<span className="font-semibold text-white">Swap control columns (put joystick on the left)</span>
|
||||
</SettingRow>
|
||||
</CardFrame>
|
||||
<CardFrame title="Macros" bodyClassName="space-y-1 p-1 text-sm">
|
||||
<CardFrame title="Driving" bodyClassName="space-y-1 p-1 text-sm">
|
||||
<SettingRow inline>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -419,7 +419,7 @@ export default function SettingsPanel() {
|
||||
checked={driveMacroBackoffEnabled}
|
||||
onChange={handleDriveMacroBackoffEnabled}
|
||||
/>
|
||||
<span className="font-semibold text-white">Enable backward bump in drive macro</span>
|
||||
<span className="font-semibold text-white">Back away from the dock when undocking</span>
|
||||
</SettingRow>
|
||||
</CardFrame>
|
||||
<CardFrame title="Audio" className="col-span-full" bodyClassName="space-y-1 p-1 text-sm">
|
||||
|
||||
Reference in New Issue
Block a user