mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
hud stuff
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
@@ -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="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-CduNi74O.js"></script>
|
<script type="module" crossorigin src="/assets/index-BCMqnb9w.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Battery Bar
|
// Battery Bar
|
||||||
// Purpose: Defines the Battery Bar module and the local helpers/components used in this file.
|
// Purpose: Defines the Battery Bar 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 React from 'react';
|
||||||
import { WARN_DISPLAY_PERCENT } from '../../lib/battery.js';
|
import { WARN_DISPLAY_PERCENT } from '../../lib/battery.js';
|
||||||
|
|
||||||
const WARN_FLASH_MS = 1600;
|
const WARN_FLASH_MS = 1600;
|
||||||
@@ -10,7 +11,7 @@ function classNames(...values) {
|
|||||||
return values.filter(Boolean).join(' ');
|
return values.filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BatteryBar({
|
function BatteryBar({
|
||||||
visual,
|
visual,
|
||||||
orientation = 'horizontal',
|
orientation = 'horizontal',
|
||||||
variant = 'inline',
|
variant = 'inline',
|
||||||
@@ -155,3 +156,5 @@ export default function BatteryBar({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(BatteryBar);
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
// Hud Chat Input
|
// Hud Chat Input
|
||||||
// Purpose: Defines the Hud Chat Input module and the local helpers/components used in this file.
|
// Purpose: Defines the Hud Chat Input 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 { useMemo, useState } from 'react';
|
import { memo, useMemo, useState } from 'react';
|
||||||
import { useChat } from '../../context/ChatContext.jsx';
|
import { useChat } from '../../context/ChatContext.jsx';
|
||||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import { useSettingsNamespace } from '../../settings/index.js';
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
|
|
||||||
export default function HudChatInput({ compact = false }) {
|
function HudChatInput({ compact = false }) {
|
||||||
const session = useSessionSelector((state) => state.session);
|
const role = useSessionSelector((state) => state.session?.role || null);
|
||||||
|
const currentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId || null);
|
||||||
|
const roverRoster = useSessionSelector((state) => state.session?.roster || []);
|
||||||
const { sendMessage, onInputFocus, onInputBlur, blurChat, registerInputRef, setTypingActive } = useChat();
|
const { sendMessage, onInputFocus, onInputBlur, blurChat, registerInputRef, setTypingActive } = useChat();
|
||||||
const { value: ttsSettings } = useSettingsNamespace('tts', { engine: 'flite', voice: 'rms', pitch: 50 });
|
const { value: ttsSettings } = useSettingsNamespace('tts', { engine: 'flite', voice: 'rms', pitch: 50 });
|
||||||
const [draft, setDraft] = useState('');
|
const [draft, setDraft] = useState('');
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const canChat = session?.role !== 'spectator';
|
const canChat = role !== 'spectator';
|
||||||
const hideHudChat = session?.role === 'spectator';
|
const hideHudChat = role === 'spectator';
|
||||||
const currentRoverId = session?.assignment?.roverId || null;
|
|
||||||
const rover = useMemo(
|
const rover = useMemo(
|
||||||
() => session?.roster?.find((entry) => String(entry.id) === String(currentRoverId)) || null,
|
() => roverRoster.find((entry) => String(entry.id) === String(currentRoverId)) || null,
|
||||||
[currentRoverId, session?.roster],
|
[currentRoverId, roverRoster],
|
||||||
);
|
);
|
||||||
const ttsSupported = Boolean(rover?.audio?.ttsEnabled);
|
const ttsSupported = Boolean(rover?.audio?.ttsEnabled);
|
||||||
const ttsPayload = useMemo(() => {
|
const ttsPayload = useMemo(() => {
|
||||||
@@ -98,3 +99,5 @@ export default function HudChatInput({ compact = false }) {
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(HudChatInput);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// 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 React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default function LowBatteryOverlay({ battery, compact = false }) {
|
function LowBatteryOverlay({ battery, compact = false }) {
|
||||||
if (!battery?.available) return null;
|
if (!battery?.available) return null;
|
||||||
if (!battery.warnActive && !battery.urgentActive) return null;
|
if (!battery.warnActive && !battery.urgentActive) return null;
|
||||||
|
|
||||||
@@ -24,3 +24,5 @@ export default function LowBatteryOverlay({ battery, compact = false }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(LowBatteryOverlay);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { OVERCURRENT_LABELS } from './constants.js';
|
import { OVERCURRENT_LABELS } from './constants.js';
|
||||||
|
|
||||||
export default function OvercurrentOverlay({ motors, fill = 0, compact = false }) {
|
function OvercurrentOverlay({ motors, fill = 0, compact = false }) {
|
||||||
if (!motors?.length) return null;
|
if (!motors?.length) return null;
|
||||||
const safeLabels = motors.map((name) => OVERCURRENT_LABELS[name] || name);
|
const safeLabels = motors.map((name) => OVERCURRENT_LABELS[name] || name);
|
||||||
const containerClass = compact ? 'w-[12rem] h-[3.5rem]' : 'w-[20rem] h-[7rem]';
|
const containerClass = compact ? 'w-[12rem] h-[3.5rem]' : 'w-[20rem] h-[7rem]';
|
||||||
@@ -29,3 +29,5 @@ export default function OvercurrentOverlay({ motors, fill = 0, compact = false }
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(OvercurrentOverlay);
|
||||||
|
|||||||
@@ -48,7 +48,15 @@ export default function VideoTile({
|
|||||||
isActiveDriver = false,
|
isActiveDriver = false,
|
||||||
idleSkipSeconds = null,
|
idleSkipSeconds = null,
|
||||||
}) {
|
}) {
|
||||||
const session = useSessionSelector((state) => state.session);
|
const discordUrl = useSessionSelector((state) => {
|
||||||
|
const socials = state.session?.socials || [];
|
||||||
|
const socialUrl =
|
||||||
|
socials.find((entry) => {
|
||||||
|
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
||||||
|
return key === 'discord';
|
||||||
|
})?.url || null;
|
||||||
|
return socialUrl || state.session?.discord?.invite || null;
|
||||||
|
});
|
||||||
const videoRef = useRef(null);
|
const videoRef = useRef(null);
|
||||||
const audioRef = useRef(null);
|
const audioRef = useRef(null);
|
||||||
const restartTimer = useRef(null);
|
const restartTimer = useRef(null);
|
||||||
@@ -92,12 +100,15 @@ export default function VideoTile({
|
|||||||
const showHudMap = hudForceMap ? true : mobileHud ? true : showHudMapDesktop;
|
const showHudMap = hudForceMap ? true : mobileHud ? true : showHudMapDesktop;
|
||||||
const batteryVisual = buildBatteryVisual({ charge: batteryCharge, config: batteryConfig });
|
const batteryVisual = buildBatteryVisual({ charge: batteryCharge, config: batteryConfig });
|
||||||
const wheelOvercurrents = sensors?.wheelOvercurrents || null;
|
const wheelOvercurrents = sensors?.wheelOvercurrents || null;
|
||||||
const overcurrentMotors =
|
const overcurrentMotors = useMemo(
|
||||||
wheelOvercurrents == null
|
() =>
|
||||||
? []
|
wheelOvercurrents == null
|
||||||
: Object.entries(wheelOvercurrents)
|
? []
|
||||||
.filter(([, active]) => Boolean(active))
|
: Object.entries(wheelOvercurrents)
|
||||||
.map(([key]) => key);
|
.filter(([, active]) => Boolean(active))
|
||||||
|
.map(([key]) => key),
|
||||||
|
[wheelOvercurrents],
|
||||||
|
);
|
||||||
const limiterCaps = overcurrentLimiter?.caps || null;
|
const limiterCaps = overcurrentLimiter?.caps || null;
|
||||||
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
|
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
|
||||||
const debugFlags = useMemo(() => {
|
const debugFlags = useMemo(() => {
|
||||||
@@ -119,9 +130,15 @@ export default function VideoTile({
|
|||||||
return Math.max(0, Math.min(1, 1 - Math.min(driveCap, auxCap)));
|
return Math.max(0, Math.min(1, 1 - Math.min(driveCap, auxCap)));
|
||||||
}, [limiterCaps]);
|
}, [limiterCaps]);
|
||||||
const limiterActive = Boolean(overcurrentLimiter?.isActive);
|
const limiterActive = Boolean(overcurrentLimiter?.isActive);
|
||||||
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
const overlayState = useMemo(() => {
|
||||||
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
const motors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
||||||
const overlayVisible = Boolean(overlayMotors.length);
|
const fill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
||||||
|
return {
|
||||||
|
motors,
|
||||||
|
fill,
|
||||||
|
visible: Boolean(motors.length),
|
||||||
|
};
|
||||||
|
}, [overcurrentMotors, limiterActive, limiterFill]);
|
||||||
const mainBrushActive = Boolean(
|
const mainBrushActive = Boolean(
|
||||||
(Number(sensors?.mainBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
|
(Number(sensors?.mainBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
|
||||||
sensors?.wheelOvercurrents?.mainBrush,
|
sensors?.wheelOvercurrents?.mainBrush,
|
||||||
@@ -196,26 +213,18 @@ export default function VideoTile({
|
|||||||
},
|
},
|
||||||
[debugAudio, label],
|
[debugAudio, label],
|
||||||
);
|
);
|
||||||
const discordUrl =
|
|
||||||
session?.socials?.find((entry) => {
|
|
||||||
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
|
||||||
return key === 'discord';
|
|
||||||
})?.url ||
|
|
||||||
session?.discord?.invite ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!debugHud) return;
|
if (!debugHud) return;
|
||||||
console.log('[OvercurrentHUD]', {
|
console.log('[OvercurrentHUD]', {
|
||||||
overlayVisible,
|
overlayVisible: overlayState.visible,
|
||||||
overlayMotors,
|
overlayMotors: overlayState.motors,
|
||||||
overlayFill,
|
overlayFill: overlayState.fill,
|
||||||
limiterActive,
|
limiterActive,
|
||||||
limiterCaps,
|
limiterCaps,
|
||||||
limiterGroups,
|
limiterGroups,
|
||||||
wheelOvercurrents,
|
wheelOvercurrents,
|
||||||
});
|
});
|
||||||
}, [debugHud, overlayFill, overlayMotors, overlayVisible, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
|
}, [debugHud, overlayState, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logAudio('settings/update');
|
logAudio('settings/update');
|
||||||
@@ -631,10 +640,12 @@ export default function VideoTile({
|
|||||||
{!noHud ? <HudChatInput compact={mobileHud} /> : null}
|
{!noHud ? <HudChatInput compact={mobileHud} /> : null}
|
||||||
{!noHud && debugHud ? (
|
{!noHud && debugHud ? (
|
||||||
<div className="pointer-events-none absolute left-1 top-1 z-40 rounded bg-black/80 px-1 py-0.5 text-[0.6rem] text-lime-200">
|
<div className="pointer-events-none absolute left-1 top-1 z-40 rounded bg-black/80 px-1 py-0.5 text-[0.6rem] text-lime-200">
|
||||||
{`OC vis:${overlayVisible ? 1 : 0} motors:${overlayMotors.length} fill:${Math.round(overlayFill * 100)}%`}
|
{`OC vis:${overlayState.visible ? 1 : 0} motors:${overlayState.motors.length} fill:${Math.round(
|
||||||
|
overlayState.fill * 100,
|
||||||
|
)}%`}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!noHud ? <OvercurrentOverlay motors={overlayMotors} fill={overlayFill} compact={mobileHud} /> : null}
|
{!noHud ? <OvercurrentOverlay motors={overlayState.motors} fill={overlayState.fill} compact={mobileHud} /> : null}
|
||||||
{!noHud ? <LowBatteryOverlay battery={batteryVisual} compact={mobileHud} /> : null}
|
{!noHud ? <LowBatteryOverlay battery={batteryVisual} compact={mobileHud} /> : null}
|
||||||
{!noHud && showVerticalBattery && batteryVisual.available ? (
|
{!noHud && showVerticalBattery && batteryVisual.available ? (
|
||||||
<div className="pointer-events-none absolute right-1 top-1/2 flex h-[70%] -translate-y-1/2 flex-col items-center justify-center rounded bg-black/60 px-0.5 pb-1 pt-1">
|
<div className="pointer-events-none absolute right-1 top-1/2 flex h-[70%] -translate-y-1/2 flex-col items-center justify-center rounded bg-black/60 px-0.5 pb-1 pt-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user