mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
gahwah
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
@@ -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-B4BVCZld.js"></script>
|
<script type="module" crossorigin src="/assets/index-BaI6Xr6X.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CQCz6Dh4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bi3rvCda.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function CameraServoPanel() {
|
|||||||
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
||||||
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
|
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
|
||||||
const hornKey = formatKeyLabel(keymap?.hornHonk?.[0]);
|
const hornKey = formatKeyLabel(keymap?.hornHonk?.[0]);
|
||||||
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
|
const hornBlocked = horn?.overheated;
|
||||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||||
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||||
const value =
|
const value =
|
||||||
@@ -107,11 +107,12 @@ export default function CameraServoPanel() {
|
|||||||
)}
|
)}
|
||||||
{hornAvailable && (
|
{hornAvailable && (
|
||||||
<HornControl
|
<HornControl
|
||||||
disabled={!roverId || hornCooldownActive}
|
disabled={!roverId || hornBlocked}
|
||||||
onStart={startHorn}
|
onStart={startHorn}
|
||||||
onStop={stopHorn}
|
onStop={stopHorn}
|
||||||
keyLabel={hornKey}
|
keyLabel={hornKey}
|
||||||
active={horn?.active}
|
active={horn?.active}
|
||||||
|
heat={horn?.heat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enabled && (
|
{enabled && (
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function InlineCameraTilt({ keymap }) {
|
|||||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||||
const nightVisionState = pipeline?.nightVisionState;
|
const nightVisionState = pipeline?.nightVisionState;
|
||||||
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
||||||
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
|
const hornBlocked = horn?.overheated;
|
||||||
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
|
||||||
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||||
const value =
|
const value =
|
||||||
@@ -134,11 +134,12 @@ export function InlineCameraTilt({ keymap }) {
|
|||||||
)}
|
)}
|
||||||
{hornAvailable && (
|
{hornAvailable && (
|
||||||
<HornControl
|
<HornControl
|
||||||
disabled={!roverId || hornCooldownActive}
|
disabled={!roverId || hornBlocked}
|
||||||
onStart={startHorn}
|
onStart={startHorn}
|
||||||
onStop={stopHorn}
|
onStop={stopHorn}
|
||||||
keyLabel={hornLabel}
|
keyLabel={hornLabel}
|
||||||
active={horn?.active}
|
active={horn?.active}
|
||||||
|
heat={horn?.heat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enabled && (
|
{enabled && (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function HornControl({
|
|||||||
onStop,
|
onStop,
|
||||||
keyLabel,
|
keyLabel,
|
||||||
active,
|
active,
|
||||||
|
heat = 0,
|
||||||
className = '',
|
className = '',
|
||||||
}) {
|
}) {
|
||||||
const [pressed, setPressed] = useState(false);
|
const [pressed, setPressed] = useState(false);
|
||||||
@@ -36,9 +37,10 @@ export default function HornControl({
|
|||||||
}, [hornSettings?.freqs, hornSettings?.waveform]);
|
}, [hornSettings?.freqs, hornSettings?.waveform]);
|
||||||
|
|
||||||
const isActive = Boolean(active);
|
const isActive = Boolean(active);
|
||||||
|
const clampedHeat = Math.max(0, Math.min(1, Number(heat) || 0));
|
||||||
const buttonClasses = useMemo(() => {
|
const buttonClasses = useMemo(() => {
|
||||||
const base =
|
const base =
|
||||||
'group flex w-full flex-col gap-0.5 rounded-xl border-2 px-1 py-1.5 text-xs font-semibold';
|
'group relative flex w-full flex-col gap-0.5 overflow-hidden rounded-xl border-2 px-1 py-1.5 text-xs font-semibold';
|
||||||
const active = 'border-fuchsia-300/70 bg-fuchsia-700 text-fuchsia-50';
|
const active = 'border-fuchsia-300/70 bg-fuchsia-700 text-fuchsia-50';
|
||||||
const inactive = 'border-cyan-300/70 bg-cyan-900 text-cyan-50 hover:bg-cyan-800';
|
const inactive = 'border-cyan-300/70 bg-cyan-900 text-cyan-50 hover:bg-cyan-800';
|
||||||
return [base, isActive ? active : inactive, 'disabled:opacity-50', className]
|
return [base, isActive ? active : inactive, 'disabled:opacity-50', className]
|
||||||
@@ -61,6 +63,12 @@ export default function HornControl({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive && pressed) {
|
||||||
|
setPressed(false);
|
||||||
|
}
|
||||||
|
}, [isActive, pressed]);
|
||||||
|
|
||||||
const formattedWaveform = waveform === 'sine' ? 'sine' : 'saw';
|
const formattedWaveform = waveform === 'sine' ? 'sine' : 'saw';
|
||||||
|
|
||||||
const updateWaveform = useCallback(
|
const updateWaveform = useCallback(
|
||||||
@@ -111,7 +119,23 @@ export default function HornControl({
|
|||||||
onBlur={stop}
|
onBlur={stop}
|
||||||
className={buttonClasses}
|
className={buttonClasses}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-0.5">
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 opacity-70"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, rgba(14,116,144,0.7) ${clampedHeat * 100}%, rgba(0,0,0,0) ${
|
||||||
|
clampedHeat * 100
|
||||||
|
}%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`pointer-events-none absolute inset-0 transition-opacity ${
|
||||||
|
isActive ? 'opacity-40' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(90deg, rgba(217,70,239,0.75), rgba(244,114,182,0.35))',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 flex items-center justify-between gap-0.5">
|
||||||
<span className="flex items-center gap-0.5 text-sm font-semibold">
|
<span className="flex items-center gap-0.5 text-sm font-semibold">
|
||||||
<span>Horn</span>
|
<span>Horn</span>
|
||||||
{keyLabel ? (
|
{keyLabel ? (
|
||||||
@@ -128,7 +152,7 @@ export default function HornControl({
|
|||||||
{isActive ? 'Honk' : 'Hold'}
|
{isActive ? 'Honk' : 'Hold'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-[minmax(0,1.2fr)_repeat(4,minmax(0,1fr))] items-center gap-0.5 text-[0.65rem]">
|
<div className="relative z-10 grid grid-cols-[minmax(0,1.2fr)_repeat(4,minmax(0,1fr))] items-center gap-0.5 text-[0.65rem]">
|
||||||
<label className="flex items-center gap-0.5 text-slate-200">
|
<label className="flex items-center gap-0.5 text-slate-200">
|
||||||
<span className="text-slate-300">Wave</span>
|
<span className="text-slate-300">Wave</span>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||||
const nightVisionState = pipeline?.nightVisionState;
|
const nightVisionState = pipeline?.nightVisionState;
|
||||||
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
||||||
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
|
const hornBlocked = horn?.overheated;
|
||||||
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
||||||
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||||
const cameraValue =
|
const cameraValue =
|
||||||
@@ -228,10 +228,11 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
)}
|
)}
|
||||||
{hornAvailable && (
|
{hornAvailable && (
|
||||||
<HornControl
|
<HornControl
|
||||||
disabled={disabled || hornCooldownActive}
|
disabled={disabled || hornBlocked}
|
||||||
onStart={startHorn}
|
onStart={startHorn}
|
||||||
onStop={stopHorn}
|
onStop={stopHorn}
|
||||||
active={horn?.active}
|
active={horn?.active}
|
||||||
|
heat={horn?.heat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cameraEnabled && (
|
{cameraEnabled && (
|
||||||
|
|||||||
@@ -2,7 +2,15 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useReducer,
|
|||||||
import { controlReducer, initialControlState } from './controlReducer.js';
|
import { controlReducer, initialControlState } from './controlReducer.js';
|
||||||
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
|
import { computeDifferentialSpeeds, clamp } from './controlMath.js';
|
||||||
import { useCommandPipeline } from './commandPipeline.js';
|
import { useCommandPipeline } from './commandPipeline.js';
|
||||||
import { DEFAULT_KEYMAP, DEFAULT_MACROS, HORN_COOLDOWN_RATIO, HORN_MAX_MS, SONG_DEFAULT_NOTE } from './constants.js';
|
import {
|
||||||
|
DEFAULT_KEYMAP,
|
||||||
|
DEFAULT_MACROS,
|
||||||
|
HORN_HEAT_COOL_PER_SEC,
|
||||||
|
HORN_HEAT_RESUME_THRESHOLD,
|
||||||
|
HORN_HEAT_UP_PER_SEC,
|
||||||
|
HORN_MAX_MS,
|
||||||
|
SONG_DEFAULT_NOTE,
|
||||||
|
} from './constants.js';
|
||||||
import { canonicalizeKeyInput } from './keymapUtils.js';
|
import { canonicalizeKeyInput } from './keymapUtils.js';
|
||||||
import { useSettingsNamespace } from '../settings/index.js';
|
import { useSettingsNamespace } from '../settings/index.js';
|
||||||
import { useSession } from '../context/SessionContext.jsx';
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
@@ -34,8 +42,8 @@ export function ControlSystemProvider({ children }) {
|
|||||||
const pendingLightsRef = useRef(false);
|
const pendingLightsRef = useRef(false);
|
||||||
const servoAngleRef = useRef(initialControlState.camera.angle);
|
const servoAngleRef = useRef(initialControlState.camera.angle);
|
||||||
const hornAutoStopRef = useRef(null);
|
const hornAutoStopRef = useRef(null);
|
||||||
const hornCooldownRef = useRef(null);
|
|
||||||
const hornStartAtRef = useRef(0);
|
const hornStartAtRef = useRef(0);
|
||||||
|
const hornHeatTickRef = useRef(0);
|
||||||
const {
|
const {
|
||||||
value: controlSettings,
|
value: controlSettings,
|
||||||
save: saveControlSettings,
|
save: saveControlSettings,
|
||||||
@@ -367,10 +375,10 @@ export function ControlSystemProvider({ children }) {
|
|||||||
|
|
||||||
const startHorn = useCallback(() => {
|
const startHorn = useCallback(() => {
|
||||||
if (!pipeline.horn) return;
|
if (!pipeline.horn) return;
|
||||||
const now = Date.now();
|
if (state.horn?.overheated) return false;
|
||||||
const cooldownUntil = state.horn?.cooldownUntil ?? 0;
|
if ((state.horn?.heat ?? 0) >= 1) return false;
|
||||||
if (cooldownUntil && now < cooldownUntil) return false;
|
|
||||||
if (state.horn?.active) return false;
|
if (state.horn?.active) return false;
|
||||||
|
const now = Date.now();
|
||||||
pipeline.sendHorn({ action: 'start', ...normalizedHornSettings });
|
pipeline.sendHorn({ action: 'start', ...normalizedHornSettings });
|
||||||
dispatch({ type: 'control/set-horn-active', payload: true });
|
dispatch({ type: 'control/set-horn-active', payload: true });
|
||||||
hornStartAtRef.current = now;
|
hornStartAtRef.current = now;
|
||||||
@@ -383,7 +391,7 @@ export function ControlSystemProvider({ children }) {
|
|||||||
}, HORN_MAX_MS);
|
}, HORN_MAX_MS);
|
||||||
recordControlIntent();
|
recordControlIntent();
|
||||||
return true;
|
return true;
|
||||||
}, [dispatch, normalizedHornSettings, pipeline, recordControlIntent, state.horn?.active, state.horn?.cooldownUntil]);
|
}, [dispatch, normalizedHornSettings, pipeline, recordControlIntent, state.horn?.active, state.horn?.heat, state.horn?.overheated]);
|
||||||
|
|
||||||
const stopHorn = useCallback(() => {
|
const stopHorn = useCallback(() => {
|
||||||
if (!pipeline.horn) return;
|
if (!pipeline.horn) return;
|
||||||
@@ -393,26 +401,38 @@ export function ControlSystemProvider({ children }) {
|
|||||||
clearTimeout(hornAutoStopRef.current);
|
clearTimeout(hornAutoStopRef.current);
|
||||||
hornAutoStopRef.current = null;
|
hornAutoStopRef.current = null;
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
|
||||||
const holdMs = Math.max(0, now - (hornStartAtRef.current || now));
|
|
||||||
hornStartAtRef.current = 0;
|
hornStartAtRef.current = 0;
|
||||||
const cooldownMs = Math.max(0, Math.round(holdMs * HORN_COOLDOWN_RATIO));
|
|
||||||
const cooldownUntil = cooldownMs > 0 ? now + cooldownMs : 0;
|
|
||||||
dispatch({ type: 'control/set-horn-cooldown', payload: cooldownUntil });
|
|
||||||
if (hornCooldownRef.current) {
|
|
||||||
clearTimeout(hornCooldownRef.current);
|
|
||||||
}
|
|
||||||
if (cooldownMs > 0) {
|
|
||||||
hornCooldownRef.current = setTimeout(() => {
|
|
||||||
dispatch({ type: 'control/set-horn-cooldown', payload: 0 });
|
|
||||||
hornCooldownRef.current = null;
|
|
||||||
}, cooldownMs);
|
|
||||||
} else {
|
|
||||||
dispatch({ type: 'control/set-horn-cooldown', payload: 0 });
|
|
||||||
hornCooldownRef.current = null;
|
|
||||||
}
|
|
||||||
}, [dispatch, pipeline]);
|
}, [dispatch, pipeline]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tickMs = 100;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const last = hornHeatTickRef.current || now;
|
||||||
|
hornHeatTickRef.current = now;
|
||||||
|
const dt = Math.min(1000, Math.max(0, now - last)) / 1000;
|
||||||
|
const currentHeat = state.horn?.heat ?? 0;
|
||||||
|
const active = Boolean(state.horn?.active);
|
||||||
|
const overheated = Boolean(state.horn?.overheated);
|
||||||
|
const delta = dt * (active ? HORN_HEAT_UP_PER_SEC : -HORN_HEAT_COOL_PER_SEC);
|
||||||
|
let nextHeat = Math.max(0, Math.min(1, currentHeat + delta));
|
||||||
|
let nextOverheated = overheated;
|
||||||
|
if (nextHeat >= 1) {
|
||||||
|
nextHeat = 1;
|
||||||
|
nextOverheated = true;
|
||||||
|
if (active) {
|
||||||
|
stopHorn();
|
||||||
|
}
|
||||||
|
} else if (nextOverheated && nextHeat <= HORN_HEAT_RESUME_THRESHOLD) {
|
||||||
|
nextOverheated = false;
|
||||||
|
}
|
||||||
|
if (nextHeat !== currentHeat || nextOverheated !== overheated) {
|
||||||
|
dispatch({ type: 'control/set-horn-heat', payload: { heat: nextHeat, overheated: nextOverheated } });
|
||||||
|
}
|
||||||
|
}, tickMs);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [dispatch, state.horn?.active, state.horn?.heat, state.horn?.overheated, stopHorn]);
|
||||||
|
|
||||||
const registerInputState = useCallback((source, data) => {
|
const registerInputState = useCallback((source, data) => {
|
||||||
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
|
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export const SONG_DEFAULT_DURATION = 8;
|
|||||||
export const SONG_REPEAT_MS = 250;
|
export const SONG_REPEAT_MS = 250;
|
||||||
|
|
||||||
export const HORN_MAX_MS = 1200;
|
export const HORN_MAX_MS = 1200;
|
||||||
export const HORN_COOLDOWN_RATIO = 0.9;
|
export const HORN_HEAT_UP_PER_SEC = 1.0;
|
||||||
|
export const HORN_HEAT_COOL_PER_SEC = 0.35;
|
||||||
|
export const HORN_HEAT_RESUME_THRESHOLD = 0.7;
|
||||||
|
|
||||||
export const OI_COMMANDS = {
|
export const OI_COMMANDS = {
|
||||||
start: [128],
|
start: [128],
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ function createSongState() {
|
|||||||
function createHornState() {
|
function createHornState() {
|
||||||
return {
|
return {
|
||||||
active: false,
|
active: false,
|
||||||
cooldownUntil: 0,
|
heat: 0,
|
||||||
|
overheated: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,12 +146,13 @@ export function controlReducer(state, action) {
|
|||||||
active: Boolean(action.payload),
|
active: Boolean(action.payload),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
case 'control/set-horn-cooldown':
|
case 'control/set-horn-heat':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
horn: {
|
horn: {
|
||||||
...(state.horn || createHornState()),
|
...(state.horn || createHornState()),
|
||||||
cooldownUntil: typeof action.payload === 'number' ? action.payload : 0,
|
heat: typeof action.payload?.heat === 'number' ? action.payload.heat : 0,
|
||||||
|
overheated: Boolean(action.payload?.overheated),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
case 'control/record-intent':
|
case 'control/record-intent':
|
||||||
|
|||||||
@@ -443,6 +443,10 @@ export default function KeyboardInputManager() {
|
|||||||
latestResetAllRef.current();
|
latestResetAllRef.current();
|
||||||
}, [state.roverId]);
|
}, [state.roverId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
hornActiveRef.current = Boolean(state.horn?.active);
|
||||||
|
}, [state.horn?.active]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
stopServoLoop();
|
stopServoLoop();
|
||||||
|
|||||||
Reference in New Issue
Block a user