mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
dock assist HUD
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useDockIr } from '../hooks/useDockIr.js';
|
||||
|
||||
const SVG_SIZE = 150;
|
||||
const CENTER_X = SVG_SIZE / 2;
|
||||
const DOCK_Y = 26;
|
||||
const ROVER_Y = 120;
|
||||
|
||||
function lobePath(side = 'left') {
|
||||
if (side === 'left') {
|
||||
return `M ${CENTER_X} ${DOCK_Y} C ${CENTER_X - 28} ${DOCK_Y + 24} ${CENTER_X - 36} ${ROVER_Y - 18} ${CENTER_X - 10} ${ROVER_Y} L ${CENTER_X} ${ROVER_Y} Z`;
|
||||
}
|
||||
return `M ${CENTER_X} ${DOCK_Y} C ${CENTER_X + 28} ${DOCK_Y + 24} ${CENTER_X + 36} ${ROVER_Y - 18} ${CENTER_X + 10} ${ROVER_Y} L ${CENTER_X} ${ROVER_Y} Z`;
|
||||
}
|
||||
|
||||
export default function DockAssistHUD({ sensors }) {
|
||||
const state = useDockIr(sensors);
|
||||
const lobe = useMemo(
|
||||
() => ({
|
||||
left: {
|
||||
base: 'rgba(16, 185, 129, 0.15)',
|
||||
active: 'rgba(16, 185, 129, 0.55)',
|
||||
outline: 'rgba(52, 211, 153, 0.9)',
|
||||
},
|
||||
right: {
|
||||
base: 'rgba(239, 68, 68, 0.15)',
|
||||
active: 'rgba(239, 68, 68, 0.55)',
|
||||
outline: 'rgba(248, 113, 113, 0.9)',
|
||||
},
|
||||
force: {
|
||||
halo: 'rgba(59, 130, 246, 0.15)',
|
||||
haloStrong: 'rgba(59, 130, 246, 0.35)',
|
||||
outline: 'rgba(191, 219, 254, 0.8)',
|
||||
},
|
||||
rover: {
|
||||
body: '#111827',
|
||||
fill: '#e5e7eb',
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
if (!state.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nudge = state.balance * 8; // pixels to offset rover cue
|
||||
const leftGlow = state.left.red || state.left.green || state.left.force;
|
||||
const rightGlow = state.right.red || state.right.green || state.right.force;
|
||||
const aligned = state.left.red && state.left.green && state.right.red && state.right.green && state.forceDetected;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 flex justify-center p-1">
|
||||
<div className="rounded bg-black/60 px-1 py-0.5 shadow-lg shadow-black/50 ring-1 ring-white/10">
|
||||
<svg width={SVG_SIZE} height={SVG_SIZE * 0.78} viewBox={`0 0 ${SVG_SIZE} ${SVG_SIZE * 0.78}`} role="img" aria-label="Docking assist">
|
||||
{/* Force field halo */}
|
||||
<circle
|
||||
cx={CENTER_X}
|
||||
cy={DOCK_Y + 6}
|
||||
r={26}
|
||||
fill={state.forceDetected ? lobe.force.haloStrong : lobe.force.halo}
|
||||
stroke={state.forceDetected ? lobe.force.outline : 'none'}
|
||||
strokeWidth={state.forceDetected ? 2 : 0}
|
||||
/>
|
||||
|
||||
{/* Left lobe */}
|
||||
<path d={lobePath('left')} fill={leftGlow ? lobe.left.active : lobe.left.base} />
|
||||
{aligned && <path d={lobePath('left')} fill="none" stroke={lobe.left.outline} strokeWidth={1.5} />}
|
||||
|
||||
{/* Right lobe */}
|
||||
<path d={lobePath('right')} fill={rightGlow ? lobe.right.active : lobe.right.base} />
|
||||
{aligned && <path d={lobePath('right')} fill="none" stroke={lobe.right.outline} strokeWidth={1.5} />}
|
||||
|
||||
{/* Dock icon */}
|
||||
<rect x={CENTER_X - 16} y={DOCK_Y - 10} width="32" height="12" rx="2" fill="#1f2937" stroke="#6b7280" strokeWidth="1" />
|
||||
<circle cx={CENTER_X - 6} cy={DOCK_Y - 4} r="1.8" fill="#9ca3af" />
|
||||
<circle cx={CENTER_X + 6} cy={DOCK_Y - 4} r="1.8" fill="#9ca3af" />
|
||||
|
||||
{/* Rover cue */}
|
||||
<g transform={`translate(${CENTER_X + nudge}, ${ROVER_Y})`}>
|
||||
<circle r="8" fill={lobe.rover.body} stroke="#6b7280" strokeWidth="1.5" />
|
||||
<path d="M 0 -6 L 4 4 L -4 4 Z" fill={lobe.rover.fill} />
|
||||
</g>
|
||||
|
||||
{/* Nudge arrow */}
|
||||
{state.bias !== 'center' && (
|
||||
<path
|
||||
d={
|
||||
state.bias === 'left'
|
||||
? `M ${CENTER_X - 18} ${ROVER_Y - 6} L ${CENTER_X - 28} ${ROVER_Y} L ${CENTER_X - 18} ${ROVER_Y + 6}`
|
||||
: `M ${CENTER_X + 18} ${ROVER_Y - 6} L ${CENTER_X + 28} ${ROVER_Y} L ${CENTER_X + 18} ${ROVER_Y + 6}`
|
||||
}
|
||||
fill="none"
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Raw code badge for debugging */}
|
||||
<g transform={`translate(${CENTER_X}, ${ROVER_Y + 18})`} className="text-xs">
|
||||
<rect x="-32" y="-10" width="64" height="18" rx="4" fill="rgba(0,0,0,0.6)" stroke="rgba(255,255,255,0.15)" />
|
||||
<text x="0" y="3" fill="#e5e7eb" fontSize="9" textAnchor="middle">
|
||||
{`L:${state.left.code ?? 0} O:${state.omni.code ?? 0} R:${state.right.code ?? 0}`}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -113,6 +113,12 @@ function SensorDetails({ sensors }) {
|
||||
|
||||
return (
|
||||
<div className="grid gap-0.5 md:grid-cols-2">
|
||||
<DetailCard title="Dock IR (raw)">
|
||||
<ValueRow label="Left" value={formatNumber(sensors?.infraredCharacterLeft)} />
|
||||
<ValueRow label="Omni" value={formatNumber(sensors?.infraredCharacterOmni)} />
|
||||
<ValueRow label="Right" value={formatNumber(sensors?.infraredCharacterRight)} />
|
||||
</DetailCard>
|
||||
|
||||
<DetailCard title="Bumps & drops">
|
||||
<ValueRow label="Bump L" value={<Pill active={bumps.bumpLeft} />} />
|
||||
<ValueRow label="Bump R" value={<Pill active={bumps.bumpRight} />} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { WhepPlayer } from '../lib/whepPlayer.js';
|
||||
import TopDownMap from './TopDownMap.jsx';
|
||||
import DockAssistHUD from './DockAssistHUD.jsx';
|
||||
import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
|
||||
|
||||
const RESTART_DELAY_MS = 2000;
|
||||
@@ -527,6 +528,13 @@ function HudOverlay({
|
||||
Song {formatNoteLabel(songNote)} <span className="text-slate-400">({songNote})</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="absolute inset-0 flex items-start justify-center">
|
||||
<DockAssistHUD sensors={sensors} />
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-start justify-center">
|
||||
<DockAssistHUD sensors={sensors} />
|
||||
</div>
|
||||
<div className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5 bg-black/80 px-0.5 py-0.5 text-slate-100">
|
||||
<span>Rover: "{label || 'Unnamed Rover'}"</span>
|
||||
{/* <span>{pulse ? 'Sensors active' : 'No recent sensors'}</span> */}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const HOLD_MS = 700;
|
||||
|
||||
const DOCK_CODES = {
|
||||
161: { red: true }, // Red buoy
|
||||
164: { green: true }, // Green buoy
|
||||
165: { force: true }, // Force field only
|
||||
168: { red: true, green: true }, // Red + green
|
||||
169: { red: true, force: true }, // Red + force
|
||||
172: { green: true, force: true }, // Green + force
|
||||
173: { red: true, green: true, force: true }, // Red + green + force
|
||||
};
|
||||
|
||||
function decodeDockBits(code) {
|
||||
if (typeof code !== 'number' || code <= 0) return null;
|
||||
const bits = DOCK_CODES[code];
|
||||
if (!bits) return null;
|
||||
return { code, ...bits };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a smoothed view of dock IR signals. Keeps the last seen code alive for HOLD_MS to mask blinks.
|
||||
*/
|
||||
export function useDockIr(sensors, options = {}) {
|
||||
const holdMs = options.holdMs || HOLD_MS;
|
||||
const [state, setState] = useState({
|
||||
left: null,
|
||||
right: null,
|
||||
omni: null,
|
||||
});
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setTick(Date.now()), Math.max(holdMs / 2, 100));
|
||||
return () => clearInterval(id);
|
||||
}, [holdMs]);
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
const left = decodeDockBits(sensors?.infraredCharacterLeft);
|
||||
const right = decodeDockBits(sensors?.infraredCharacterRight);
|
||||
const omni = decodeDockBits(sensors?.infraredCharacterOmni);
|
||||
if (!left && !right && !omni) {
|
||||
return;
|
||||
}
|
||||
setState((prev) => ({
|
||||
left: left ? { ...left, ts: now } : prev.left,
|
||||
right: right ? { ...right, ts: now } : prev.right,
|
||||
omni: omni ? { ...omni, ts: now } : prev.omni,
|
||||
}));
|
||||
}, [sensors?.infraredCharacterLeft, sensors?.infraredCharacterRight, sensors?.infraredCharacterOmni]);
|
||||
|
||||
useEffect(() => {
|
||||
// Debug: log when new codes appear
|
||||
const { left, right, omni } = state;
|
||||
const haveAny = left?.ts || right?.ts || omni?.ts;
|
||||
if (!haveAny) return;
|
||||
const stamp = new Date().toISOString();
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('[DockIR]', stamp, {
|
||||
left: left?.code ?? 0,
|
||||
omni: omni?.code ?? 0,
|
||||
right: right?.code ?? 0,
|
||||
});
|
||||
}, [state.left?.code, state.right?.code, state.omni?.code]);
|
||||
|
||||
return useMemo(() => {
|
||||
const now = Date.now();
|
||||
const withWindow = (entry) => {
|
||||
if (!entry) return { active: false, red: false, green: false, force: false, age: null, code: null };
|
||||
const age = now - entry.ts;
|
||||
const active = age <= holdMs;
|
||||
return {
|
||||
active,
|
||||
age,
|
||||
code: entry.code,
|
||||
red: Boolean(entry.red && active),
|
||||
green: Boolean(entry.green && active),
|
||||
force: Boolean(entry.force && active),
|
||||
};
|
||||
};
|
||||
|
||||
const left = withWindow(state.left);
|
||||
const right = withWindow(state.right);
|
||||
const omni = withWindow(state.omni);
|
||||
const visible = left.active || right.active || omni.active;
|
||||
const forceDetected = left.force || right.force || omni.force;
|
||||
const bias =
|
||||
left.active && !right.active
|
||||
? 'right'
|
||||
: right.active && !left.active
|
||||
? 'left'
|
||||
: 'center';
|
||||
const balance = (right.active ? 1 : 0) - (left.active ? 1 : 0); // positive means steer left-to-right
|
||||
|
||||
return {
|
||||
visible,
|
||||
forceDetected,
|
||||
left,
|
||||
right,
|
||||
omni,
|
||||
bias, // left/right/center for steering cue
|
||||
balance: Math.max(-1, Math.min(1, balance)),
|
||||
};
|
||||
}, [state.left, state.right, state.omni, holdMs, tick]);
|
||||
}
|
||||
Reference in New Issue
Block a user