mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -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-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-B4BVCZld.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CQCz6Dh4.css">
|
||||
<script type="module" crossorigin src="/assets/index-BaI6Xr6X.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bi3rvCda.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function CameraServoPanel() {
|
||||
const hornAvailable = Boolean(roverId && pipeline?.horn);
|
||||
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[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 max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||
const value =
|
||||
@@ -107,11 +107,12 @@ export default function CameraServoPanel() {
|
||||
)}
|
||||
{hornAvailable && (
|
||||
<HornControl
|
||||
disabled={!roverId || hornCooldownActive}
|
||||
disabled={!roverId || hornBlocked}
|
||||
onStart={startHorn}
|
||||
onStop={stopHorn}
|
||||
keyLabel={hornKey}
|
||||
active={horn?.active}
|
||||
heat={horn?.heat}
|
||||
/>
|
||||
)}
|
||||
{enabled && (
|
||||
|
||||
@@ -85,7 +85,7 @@ export function InlineCameraTilt({ keymap }) {
|
||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||
const nightVisionState = pipeline?.nightVisionState;
|
||||
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 max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
|
||||
const value =
|
||||
@@ -134,11 +134,12 @@ export function InlineCameraTilt({ keymap }) {
|
||||
)}
|
||||
{hornAvailable && (
|
||||
<HornControl
|
||||
disabled={!roverId || hornCooldownActive}
|
||||
disabled={!roverId || hornBlocked}
|
||||
onStart={startHorn}
|
||||
onStop={stopHorn}
|
||||
keyLabel={hornLabel}
|
||||
active={horn?.active}
|
||||
heat={horn?.heat}
|
||||
/>
|
||||
)}
|
||||
{enabled && (
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function HornControl({
|
||||
onStop,
|
||||
keyLabel,
|
||||
active,
|
||||
heat = 0,
|
||||
className = '',
|
||||
}) {
|
||||
const [pressed, setPressed] = useState(false);
|
||||
@@ -36,9 +37,10 @@ export default function HornControl({
|
||||
}, [hornSettings?.freqs, hornSettings?.waveform]);
|
||||
|
||||
const isActive = Boolean(active);
|
||||
const clampedHeat = Math.max(0, Math.min(1, Number(heat) || 0));
|
||||
const buttonClasses = useMemo(() => {
|
||||
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 inactive = 'border-cyan-300/70 bg-cyan-900 text-cyan-50 hover:bg-cyan-800';
|
||||
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 updateWaveform = useCallback(
|
||||
@@ -111,7 +119,23 @@ export default function HornControl({
|
||||
onBlur={stop}
|
||||
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>Horn</span>
|
||||
{keyLabel ? (
|
||||
@@ -128,7 +152,7 @@ export default function HornControl({
|
||||
{isActive ? 'Honk' : 'Hold'}
|
||||
</span>
|
||||
</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">
|
||||
<span className="text-slate-300">Wave</span>
|
||||
<select
|
||||
|
||||
@@ -146,7 +146,7 @@ function MobileJoystickPanel({ layout }) {
|
||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||
const nightVisionState = pipeline?.nightVisionState;
|
||||
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 cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||
const cameraValue =
|
||||
@@ -228,10 +228,11 @@ function MobileJoystickPanel({ layout }) {
|
||||
)}
|
||||
{hornAvailable && (
|
||||
<HornControl
|
||||
disabled={disabled || hornCooldownActive}
|
||||
disabled={disabled || hornBlocked}
|
||||
onStart={startHorn}
|
||||
onStop={stopHorn}
|
||||
active={horn?.active}
|
||||
heat={horn?.heat}
|
||||
/>
|
||||
)}
|
||||
{cameraEnabled && (
|
||||
|
||||
@@ -2,7 +2,15 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useReducer,
|
||||
import { controlReducer, initialControlState } from './controlReducer.js';
|
||||
import { computeDifferentialSpeeds, clamp } from './controlMath.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 { useSettingsNamespace } from '../settings/index.js';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
@@ -34,8 +42,8 @@ export function ControlSystemProvider({ children }) {
|
||||
const pendingLightsRef = useRef(false);
|
||||
const servoAngleRef = useRef(initialControlState.camera.angle);
|
||||
const hornAutoStopRef = useRef(null);
|
||||
const hornCooldownRef = useRef(null);
|
||||
const hornStartAtRef = useRef(0);
|
||||
const hornHeatTickRef = useRef(0);
|
||||
const {
|
||||
value: controlSettings,
|
||||
save: saveControlSettings,
|
||||
@@ -367,10 +375,10 @@ export function ControlSystemProvider({ children }) {
|
||||
|
||||
const startHorn = useCallback(() => {
|
||||
if (!pipeline.horn) return;
|
||||
const now = Date.now();
|
||||
const cooldownUntil = state.horn?.cooldownUntil ?? 0;
|
||||
if (cooldownUntil && now < cooldownUntil) return false;
|
||||
if (state.horn?.overheated) return false;
|
||||
if ((state.horn?.heat ?? 0) >= 1) return false;
|
||||
if (state.horn?.active) return false;
|
||||
const now = Date.now();
|
||||
pipeline.sendHorn({ action: 'start', ...normalizedHornSettings });
|
||||
dispatch({ type: 'control/set-horn-active', payload: true });
|
||||
hornStartAtRef.current = now;
|
||||
@@ -383,7 +391,7 @@ export function ControlSystemProvider({ children }) {
|
||||
}, HORN_MAX_MS);
|
||||
recordControlIntent();
|
||||
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(() => {
|
||||
if (!pipeline.horn) return;
|
||||
@@ -393,26 +401,38 @@ export function ControlSystemProvider({ children }) {
|
||||
clearTimeout(hornAutoStopRef.current);
|
||||
hornAutoStopRef.current = null;
|
||||
}
|
||||
const now = Date.now();
|
||||
const holdMs = Math.max(0, now - (hornStartAtRef.current || now));
|
||||
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]);
|
||||
|
||||
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) => {
|
||||
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 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 = {
|
||||
start: [128],
|
||||
|
||||
@@ -30,7 +30,8 @@ function createSongState() {
|
||||
function createHornState() {
|
||||
return {
|
||||
active: false,
|
||||
cooldownUntil: 0,
|
||||
heat: 0,
|
||||
overheated: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,12 +146,13 @@ export function controlReducer(state, action) {
|
||||
active: Boolean(action.payload),
|
||||
},
|
||||
};
|
||||
case 'control/set-horn-cooldown':
|
||||
case 'control/set-horn-heat':
|
||||
return {
|
||||
...state,
|
||||
horn: {
|
||||
...(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':
|
||||
|
||||
@@ -443,6 +443,10 @@ export default function KeyboardInputManager() {
|
||||
latestResetAllRef.current();
|
||||
}, [state.roverId]);
|
||||
|
||||
useEffect(() => {
|
||||
hornActiveRef.current = Boolean(state.horn?.active);
|
||||
}, [state.horn?.active]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
stopServoLoop();
|
||||
|
||||
Reference in New Issue
Block a user