This commit is contained in:
legop3
2026-03-21 00:06:22 -04:00
parent d6da071fde
commit 36a6c07f95
8 changed files with 128 additions and 107 deletions
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
+2 -2
View File
@@ -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-BY461TBg.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DzG-Bp9_.css">
<script type="module" crossorigin src="/assets/index-Bg3-HsWj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B89HtNlO.css">
</head>
<body>
<div id="root"></div>
+11 -4
View File
@@ -6,7 +6,12 @@ import MobileControls, {
MobileLeftColumn,
MobileRightColumn,
} from './components/MobileControls.jsx';
import { ControlSystemProvider, KeyboardInputManager, GamepadInputManager } from './controls/index.js';
import {
ControlSystemProvider,
KeyboardInputManager,
GamepadInputManager,
useControlSystem,
} from './controls/index.js';
import RoomCameraPanel from './components/RoomCameraPanel.jsx';
import LogPanel from './components/LogPanel.jsx';
import DriverVideoPanel from './components/DriverVideoPanel.jsx';
@@ -84,12 +89,14 @@ function MobileFeatureTabs({
showTelemetry = true,
}) {
const { session } = useSession();
const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false });
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-amber-400';
const ownRoverId = String(session?.assignment?.roverId || '').trim();
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const vipMicActive = Boolean(
ownAudioForward?.source === 'mic-whip' &&
(ownAudioForward?.state === 'starting' || ownAudioForward?.state === 'playing'),
ownRoverId && session?.isVerified && (openMicEnabled || pttActive),
);
return (
<section className="panel text-base">
+6 -3
View File
@@ -18,6 +18,7 @@ import HornControl from './HornControl.jsx';
import CameraTiltControl from './CameraTiltControl.jsx';
import VipPanel from './VipPanel.jsx';
import { useSession } from '../context/SessionContext.jsx';
import { useSettingsNamespace } from '../settings/index.js';
function TopDownMapPanel() {
const {
@@ -111,12 +112,14 @@ function DriveDockPanel() {
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const { session } = useSession();
const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false });
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-red-600';
const ownRoverId = String(session?.assignment?.roverId || '').trim();
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const vipMicActive = Boolean(
ownAudioForward?.source === 'mic-whip' &&
(ownAudioForward?.state === 'starting' || ownAudioForward?.state === 'playing'),
ownRoverId && session?.isVerified && (openMicEnabled || pttActive),
);
return (
<section className="panel text-base">
+3 -12
View File
@@ -48,7 +48,7 @@ export default function VipPanel() {
<section className="surface w-full">
<div className="mx-auto flex w-full max-w-md flex-col items-center space-y-0.5 text-center">
<p className="text-sm text-emerald-300">Verification complete</p>
<p className="text-xs text-slate-400">VIP audio controls are unlocked for your assigned rover.</p>
<p className="text-xs text-slate-400">VIP controls are unlocked for your assigned rover.</p>
</div>
</section>
) : (
@@ -64,6 +64,7 @@ export default function VipPanel() {
)}
</div>
<div className="lg:col-span-2">
{isVerified ? (
<VipAudioUploadCard
ownRoverId={ownRoverId}
@@ -77,21 +78,11 @@ export default function VipPanel() {
) : (
<section className="surface h-full">
<div className="flex h-full flex-col items-center justify-center text-center text-xs text-slate-400">
Verify your account to unlock audio upload and microphone forwarding.
Verify your account to unlock VIP controls.
</div>
</section>
)}
<section className="surface h-full">
<div className="space-y-0.5">
<p className="text-sm text-slate-200">VIP Notes</p>
<ul className="space-y-0.25 text-xs text-slate-400">
<li>Audio forwarding is locked to your currently assigned rover.</li>
<li>Losing control or turn ownership stops active forwarding.</li>
<li>PTT works page-wide using your keybind (default: m).</li>
</ul>
</div>
</section>
<div className="lg:col-span-2">
<VipIdentityCard
+46 -26
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { fieldClass } from './constants.js';
import { useControlSystem } from '../../controls/index.js';
import { useSettingsNamespace } from '../../settings/index.js';
const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
const TARGET_SAMPLE_RATE = 16000;
@@ -154,14 +155,17 @@ function waitForOutboundAudioFlow(pc, timeoutMs = 6000) {
});
}
function StatusLight({ label, active, detail = '' }) {
function StatusIndicator({ label, active, detail = '' }) {
return (
<div className="surface-muted flex items-center justify-between px-0.5 py-0.25 text-xs">
<span className="text-slate-300">{label}</span>
<span className="inline-flex items-center gap-0.5">
<span className={`h-2 w-2 rounded-full ${active ? 'bg-emerald-400' : 'bg-slate-600'}`} aria-hidden="true" />
<span className={active ? 'text-emerald-300' : 'text-slate-500'}>{detail || (active ? 'on' : 'off')}</span>
</span>
<div
className={`rounded-md border px-0.5 py-0.5 text-xs transition-colors ${
active
? 'border-emerald-300/80 bg-emerald-500/20 text-emerald-200'
: 'border-slate-700 bg-slate-900/50 text-slate-400'
}`}
>
<div className="font-medium">{label}</div>
<div className="text-[0.72rem] opacity-90">{detail || (active ? 'active' : 'idle')}</div>
</div>
);
}
@@ -176,10 +180,12 @@ export default function VipAudioUploadCard({
stopMicWhip,
}) {
const { state: controlState } = useControlSystem();
const { value: vipAudio, save: saveVipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false });
const roverId = String(ownRoverId || '').trim();
const [selectedUpload, setSelectedUpload] = useState(null);
const [working, setWorking] = useState(false);
const [openMicEnabled, setOpenMicEnabled] = useState(false);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const [micState, setMicState] = useState('idle');
const [message, setMessage] = useState('');
const streamRef = useRef(null);
@@ -204,7 +210,8 @@ export default function VipAudioUploadCard({
selectedForwardState?.source === 'mic-whip' &&
(selectedForwardState?.state === 'starting' || selectedForwardState?.state === 'playing'),
);
const micTalking = Boolean(audioTrackRef.current && (openMicEnabled || pttActive));
const micHot = Boolean(audioTrackRef.current && (openMicEnabled || pttActive));
const whipLinkActive = micState === 'live' || micState === 'starting';
const handleUploadPlay = async () => {
if (!roverId) {
@@ -427,9 +434,10 @@ export default function VipAudioUploadCard({
);
return (
<div className="grid gap-0.5 grid-cols-1 lg:grid-cols-2">
<section className="surface h-full">
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_auto_1fr_auto]">
<p className="text-sm text-slate-200 text-center">VIP Audio Forwarding</p>
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_1fr_auto]">
<p className="text-sm text-slate-200 text-center">Audio Upload</p>
<label className="mx-auto grid w-full max-w-sm gap-0.5 text-xs text-slate-300 text-center">
<span>Audio file (mp3 / wav / ogg)</span>
@@ -442,11 +450,15 @@ export default function VipAudioUploadCard({
/>
</label>
<div className="mx-auto w-full max-w-sm text-center">
{selectedUpload ? (
<div className="surface-muted mx-auto w-full max-w-sm text-xs text-slate-300 text-center">
<div className="surface-muted text-xs text-slate-300">
{selectedUpload.name} ({selectedUpload.size} bytes)
</div>
) : null}
) : (
<div className="surface-muted text-xs text-slate-500">No file selected</div>
)}
</div>
<div className="flex justify-center gap-0.5">
<button type="button" className="button-dark text-sm" disabled={working || !roverId} onClick={handleUploadPlay}>
@@ -456,34 +468,42 @@ export default function VipAudioUploadCard({
Stop
</button>
</div>
<div className="grid gap-0.5 lg:grid-cols-2">
<div className="space-y-0.5">
<p className="text-xs text-slate-300 text-center">Audio Path Status</p>
<StatusLight label="Forward pipe" active={pipelineConnected} detail={pipelineConnected ? 'connected' : 'offline'} />
<StatusLight label="Upload playing" active={uploadPlaying} detail={uploadPlaying ? 'active' : 'idle'} />
<StatusLight label="Mic relay" active={micRelayActive} detail={micRelayActive ? 'active' : 'idle'} />
</div>
</section>
<div className="space-y-0.5">
<p className="text-xs text-slate-300 text-center">Microphone</p>
<label className="surface-muted flex items-center justify-center gap-0.5 px-0.5 py-0.25 text-xs text-slate-300">
<section className="surface h-full">
<div className="grid h-full gap-0.5 grid-rows-[auto_auto_1fr]">
<p className="text-sm text-slate-200 text-center">Live Microphone</p>
<label className="surface-muted mx-auto flex w-full max-w-sm items-center justify-center gap-0.5 px-0.5 py-0.5 text-xs text-slate-300">
<input
type="checkbox"
checked={openMicEnabled}
disabled={!roverId}
onChange={(event) => setOpenMicEnabled(Boolean(event.target.checked))}
onChange={(event) => saveVipAudio((current) => ({ ...(current || {}), openMicEnabled: Boolean(event.target.checked) }))}
/>
<span>Open mic</span>
</label>
<StatusLight label="WHIP link" active={micState === 'live' || micState === 'starting'} detail={micState} />
<StatusLight label="PTT transmit" active={micTalking} detail={micTalking ? 'talking' : 'muted'} />
<div className="grid gap-0.5 content-start">
<StatusIndicator label="WHIP link" active={whipLinkActive} detail={micState} />
<StatusIndicator label="Mic hot" active={micHot} detail={micHot ? 'transmitting' : 'muted'} />
<div className="surface-muted text-center text-xs text-slate-400">PTT key: {controlState?.keymap?.micPtt?.[0] || 'm'} (hold)</div>
</div>
</div>
</section>
<section className="surface lg:col-span-2">
<div className="space-y-0.5">
<p className="text-sm text-slate-200 text-center">Audio Status</p>
<div className="grid gap-0.5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-5">
<StatusIndicator label="Forward pipe" active={pipelineConnected} detail={pipelineConnected ? 'connected' : 'offline'} />
<StatusIndicator label="Upload playback" active={uploadPlaying} detail={uploadPlaying ? 'playing' : 'idle'} />
<StatusIndicator label="Mic relay" active={micRelayActive} detail={micRelayActive ? 'active' : 'idle'} />
<StatusIndicator label="WHIP transport" active={whipLinkActive} detail={micState} />
<StatusIndicator label="Mic transmit" active={micHot} detail={micHot ? 'hot' : 'muted'} />
</div>
{message ? <div className="text-xs text-slate-400 text-center">{message}</div> : null}
</div>
</section>
</div>
);
}