mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -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-title" content="Multi Roomba Rover" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Battery Bar
|
||||
// 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.
|
||||
import React from 'react';
|
||||
import { WARN_DISPLAY_PERCENT } from '../../lib/battery.js';
|
||||
|
||||
const WARN_FLASH_MS = 1600;
|
||||
@@ -10,7 +11,7 @@ function classNames(...values) {
|
||||
return values.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function BatteryBar({
|
||||
function BatteryBar({
|
||||
visual,
|
||||
orientation = 'horizontal',
|
||||
variant = 'inline',
|
||||
@@ -155,3 +156,5 @@ export default function BatteryBar({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BatteryBar);
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
// Hud Chat Input
|
||||
// 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.
|
||||
import { useMemo, useState } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useChat } from '../../context/ChatContext.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
|
||||
export default function HudChatInput({ compact = false }) {
|
||||
const session = useSessionSelector((state) => state.session);
|
||||
function HudChatInput({ compact = false }) {
|
||||
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 { value: ttsSettings } = useSettingsNamespace('tts', { engine: 'flite', voice: 'rms', pitch: 50 });
|
||||
const [draft, setDraft] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const canChat = session?.role !== 'spectator';
|
||||
const hideHudChat = session?.role === 'spectator';
|
||||
const currentRoverId = session?.assignment?.roverId || null;
|
||||
const canChat = role !== 'spectator';
|
||||
const hideHudChat = role === 'spectator';
|
||||
const rover = useMemo(
|
||||
() => session?.roster?.find((entry) => String(entry.id) === String(currentRoverId)) || null,
|
||||
[currentRoverId, session?.roster],
|
||||
() => roverRoster.find((entry) => String(entry.id) === String(currentRoverId)) || null,
|
||||
[currentRoverId, roverRoster],
|
||||
);
|
||||
const ttsSupported = Boolean(rover?.audio?.ttsEnabled);
|
||||
const ttsPayload = useMemo(() => {
|
||||
@@ -98,3 +99,5 @@ export default function HudChatInput({ compact = false }) {
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(HudChatInput);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import React from 'react';
|
||||
|
||||
export default function LowBatteryOverlay({ battery, compact = false }) {
|
||||
function LowBatteryOverlay({ battery, compact = false }) {
|
||||
if (!battery?.available) return null;
|
||||
if (!battery.warnActive && !battery.urgentActive) return null;
|
||||
|
||||
@@ -24,3 +24,5 @@ export default function LowBatteryOverlay({ battery, compact = false }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(LowBatteryOverlay);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
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;
|
||||
const safeLabels = motors.map((name) => OVERCURRENT_LABELS[name] || name);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(OvercurrentOverlay);
|
||||
|
||||
@@ -48,7 +48,15 @@ export default function VideoTile({
|
||||
isActiveDriver = false,
|
||||
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 audioRef = useRef(null);
|
||||
const restartTimer = useRef(null);
|
||||
@@ -92,12 +100,15 @@ export default function VideoTile({
|
||||
const showHudMap = hudForceMap ? true : mobileHud ? true : showHudMapDesktop;
|
||||
const batteryVisual = buildBatteryVisual({ charge: batteryCharge, config: batteryConfig });
|
||||
const wheelOvercurrents = sensors?.wheelOvercurrents || null;
|
||||
const overcurrentMotors =
|
||||
wheelOvercurrents == null
|
||||
? []
|
||||
: Object.entries(wheelOvercurrents)
|
||||
.filter(([, active]) => Boolean(active))
|
||||
.map(([key]) => key);
|
||||
const overcurrentMotors = useMemo(
|
||||
() =>
|
||||
wheelOvercurrents == null
|
||||
? []
|
||||
: Object.entries(wheelOvercurrents)
|
||||
.filter(([, active]) => Boolean(active))
|
||||
.map(([key]) => key),
|
||||
[wheelOvercurrents],
|
||||
);
|
||||
const limiterCaps = overcurrentLimiter?.caps || null;
|
||||
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
|
||||
const debugFlags = useMemo(() => {
|
||||
@@ -119,9 +130,15 @@ export default function VideoTile({
|
||||
return Math.max(0, Math.min(1, 1 - Math.min(driveCap, auxCap)));
|
||||
}, [limiterCaps]);
|
||||
const limiterActive = Boolean(overcurrentLimiter?.isActive);
|
||||
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
||||
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
||||
const overlayVisible = Boolean(overlayMotors.length);
|
||||
const overlayState = useMemo(() => {
|
||||
const motors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
||||
const fill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
||||
return {
|
||||
motors,
|
||||
fill,
|
||||
visible: Boolean(motors.length),
|
||||
};
|
||||
}, [overcurrentMotors, limiterActive, limiterFill]);
|
||||
const mainBrushActive = Boolean(
|
||||
(Number(sensors?.mainBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
|
||||
sensors?.wheelOvercurrents?.mainBrush,
|
||||
@@ -196,26 +213,18 @@ export default function VideoTile({
|
||||
},
|
||||
[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(() => {
|
||||
if (!debugHud) return;
|
||||
console.log('[OvercurrentHUD]', {
|
||||
overlayVisible,
|
||||
overlayMotors,
|
||||
overlayFill,
|
||||
overlayVisible: overlayState.visible,
|
||||
overlayMotors: overlayState.motors,
|
||||
overlayFill: overlayState.fill,
|
||||
limiterActive,
|
||||
limiterCaps,
|
||||
limiterGroups,
|
||||
wheelOvercurrents,
|
||||
});
|
||||
}, [debugHud, overlayFill, overlayMotors, overlayVisible, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
|
||||
}, [debugHud, overlayState, limiterActive, limiterCaps, limiterGroups, wheelOvercurrents]);
|
||||
|
||||
useEffect(() => {
|
||||
logAudio('settings/update');
|
||||
@@ -631,10 +640,12 @@ export default function VideoTile({
|
||||
{!noHud ? <HudChatInput compact={mobileHud} /> : null}
|
||||
{!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">
|
||||
{`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>
|
||||
) : 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 && 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">
|
||||
|
||||
Reference in New Issue
Block a user